20200330のAndroidに関する記事は4件です。

ViewModelProviderクラスを定義せずにViewModelを生成する

はじめに

つい最近ViewModelを触り始めたのですが、当初は律儀にViewModelProvider(s)を使っていました。
しかし、fragment-ktxを使うことでかなり簡素にViewModelを生成出来たので自分用に纏めておきます。

実装

jvmTargetを1.8に指定し、lifecycle-viewmodel-ktxfragment-ktxを追加する。

build.gradle
android {
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    implementation "androidx.fragment:fragment-ktx:1.2.3"
}

引数なしの場合

まずは引数なしのViewModelから。
とりあえず引数なしのViewModelを定義する。
initとhelloメソッド呼び出しのタイミングでそれぞれログを出力させる。

MainViewModle.kt
class MainViewModel : ViewModel() {
    init {
        Log.d("MainViewModel", "init")
    }

    fun hello() {
        Log.d("MainViewModel", "hello")
    }
}

1行サクッと書くだけでViewModelが取得できる。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels() // ← ここ!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.hello()
    }
}

initとhelloメソッド呼び出しのタイミングでそれぞれログが出力されているのがわかる。

log
2020-03-30 22:48:02.199 3963-3963/com.masaibar.viewmodelsample D/MainViewModel: init
2020-03-30 22:48:02.199 3963-3963/com.masaibar.viewmodelsample D/MainViewModel: hello

引数ありの場合

続いて引数ありの場合、先程のViewModelのコンストラクタをuserIdという引数を受け付けるように変更した。

MainViewModle.kt
class MainViewModel(
    private val userId: String
) : ViewModel() {
    init {
        Log.d("MainViewModel", "init, userId: $userId")
    }

    fun hello() {
        Log.d("MainViewModel", "hello, userId: $userId")
    }
}

今度は先程のように1行だけサクッとという訳にはいかないが
それでもViewModelProviderクラスを定義しなくてもこのように書くことが出来る。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels {
        object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                @Suppress("UNCHECKED_CAST")
                return MainViewModel("masaibar") as T
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.hello()
    }
}

initとhelloメソッド呼び出しのタイミングでそれぞれログが出力されuserIdが渡っていることが確認できる。

log
2020-03-30 22:58:52.525 4979-4979/? D/MainViewModel: init, userId: masaibar
2020-03-30 22:58:52.525 4979-4979/? D/MainViewModel: hello, userId: masaibar

おわりに

Googleにはもう少しこれを推していって欲しいです。

参考:https://developer.android.com/kotlin/ktx?hl=ja#fragment

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【HelloAR】Unity 2019でARCore SDK for Unity v1.16.0 を実装する

はじめに

久々にARを実装してみようと思いました。Google公式 Quickstart for Androidを見れば簡単に作れますね。

環境

  • Unity 2019.3.7f1
  • ARCore SDK for Unity v1.16.0
  • Google Pixel 3a

Unityの環境を確認し、必要なパッケージをダウンロードする

モジュールはUnity Hubからダウンロードする

Unity 2019を使う場合はMultiplayer HLAPIXR Legacy Input Helperのパッケージが必要なのでダウウンロードしましょう。

WindowPackage Manager
image.png

Multiplayer HLAPI
image.png

XR Legacy Input Helpers
dimage.png

ARCore SDK for Unity v1.16.0をダウンロードする

こちらからダウンロードしてください。自動的にインポートされない場合は、ダウンロードしたUnityパッケージをAssetsImport PackageCustom Packageで選択してインポートしてください。

image.png

ビルド準備

今回実装するのはHelloARなのでダブルクリックしてシーンを合わせます。
image.png

FileBuild SettingsからAndroidSwitch Platformしましょう。少し時間がかかります。終わったらScenes in BuildHelloARにします。
image.png

Build Settingsの状態から左したのPlayer Settingsをクリックしてみましょう。以下のような画面が出てきます。

image.png

以下の設定通りに変えましょう。Vulkanは必要ないので消しましょう。Package Nameは何でも大丈夫です。Minimum API LevelAndroid 7.0以上に設定しましょう。ARCore Supportedにはチェックを入れましょう。
image.png

ビルドしよう!

ビルドできるようにした状態のAndroidをしっかりUSBで接続しましょう。そしてBuild And Runで実行!ビルドに成功したら、自動的にアプリが立ち上がります。少し時間がかかりますが、その間は何も触れないようにしましょう。

あれれ…ドロイド君じゃなくなった…!?

93664.jpg

上手くビルドできない場合はAndroidの開発者向けオプションonになっているか。USBデバッグonになっているか。これらを確認しましょう。詳しくは参考文献を見てください。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Activity内に定義したAsyncTaskのネストクラスに警告「This 'AsyncTask' class should be static or leaks might occur」

この記事は、
非staticネストクラスが握っちゃう、エンクロージングオブジェクトの暗黙的参照」と題した投稿の続きです。
ですので、先にそっちをご覧いただければ幸いです。
そっちの記事は、頭ごなし的に言っちゃえば、「ネストクラスには、static修飾子を付けろ」です。

いきなりカウントアップしだすアプリを作ってみた、ら...

起動したとたんに、勝手に1秒刻みでカウントアップが始まります。
START!から始まったら「9」まで数え上げたらお終いです。
(このアニメーションGIFは、3まで至ったらまたSTART!を繰り返しているアニメーションですが、このQiitaに貼り付けたアニメーションGIFを止めるすべがないのでごめんなさい)

countup.gif

環境は以下の通りです。

Android Studio 3.6.1
Build #AI-192.7142.36.36.6241897, built on February 27, 2020
Runtime version: 1.8.0_212-release-1586-b04 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0
GC: ParNew, ConcurrentMarkSweep
Memory: 1237M
Cores: 8
Registry: ide.new.welcome.screen.force=true
Non-Bundled Plugins:

Kotlinで開発しません。Javaでやります。

Activityのコード上に警告が出ます

このアプリのActivityは以下の通りです。

MainActivity.java
public class MainActivity extends AppCompatActivity {

    private TextView textView;

    private MyTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.text_view);
        task = new MyTask();
        task.execute();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        task.cancel(true);
    }

    class MyTask extends AsyncTask<Void, String, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            for (int i = 0; i < 10; i++) {                if (isCancelled()) {
                    return null; // break;でもいいかも
                }
                try {
                    Thread.sleep(1000);
                    publishProgress(String.valueOf(i));
                } catch (InterruptedException e) {
                    Log.e("MyTask", e.getMessage(), e);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            textView.setText(values[0]);
        }
    }
}

そしたらAndroid Studioがこんな警告を出すんです。

thisasynctaskclassshouldbestaticorleaksmightoccur.png

This 'AsyncTask' class should be static or leaks might occur

MyTaskと名付けたネストクラスに、static修飾子が付いていないことを咎めています。

この警告を出さないようにする(した)のが、この記事の目的です。

これがベストな(主流の)解決方法のようです

まず先に、結論のコードを掲載します。
エンクロージングクラスであるMainActicityオブジェクトの弱い参照を、staticネストクラスのオブジェクトは保持することにします。ということで、java.lang.ref.WeakReferenceを使います。

最善を尽くした修正
public class MainActivityGood extends AppCompatActivity {

    // TextViewのフィールドはやめます。

    private MyTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.text_view);
        // this渡してる?なんで?それはね、MyTaskにコンストラクタを設けたからさ。
        task = new MyTask(this);
        task.execute(textView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        task.cancel(true);
    }

    static class MyTask extends AsyncTask<Void, String, Void> {

        WeakReference<Activity> activityReference; // ここ注目!

        // コンストラクタを設けました。
        public MyTask(Activity activity) {
            // 弱い参照でエンクロージングクラスのオブジェクトを保持することにします。
            activityReference= new WeakReference<>(activity);
        }

        @Override
        protected Void doInBackground(Void... voids) {
            for (int i = 0; i < 10; i++) {                if (isCancelled()) {
                    return null; // break;でもいいかも
                }
                try {
                    Thread.sleep(1000);
                    publishProgress(String.valueOf(i));
                } catch (InterruptedException e) {
                    Log.e("MyTask", e.getMessage(), e);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            // エンクロージングクラスのオブジェクトの弱い参照は、getメソッドで取得できます。
            Activity activity = activityReference.get();
            // そして、その取得した弱い参照がnullになっちゃってないかのチェックはしておきましょう。
            if (activity == null || activity.isFinishing()) {
                return; // ※
            }

            TextView textView = activity.findViewById(R.id.text_view);
            textView.setText(values[0]);
        }
    }
}

結論はこうなんですが、せっかくなので途中私がこの結論に至るまでの逡巡を挟みつつ、最後には大団円を迎えたいと思います。

ネストクラスにstatic修飾子を付ける

Android Studioの警告の仰せの通りに、盲目的に(笑)MyTaskと名付けたAsyncTaskサブクラスにstatic修飾子を付けました。

ただし、それだけの措置を施しただけですと、エンクロージングクラスの非staticフィールドをアクセスする際に以下の画像のように怒られます。なぜなのか。それは、 staticネストクラスは、エンクロージングのstaticメンバーにのみアクセス可能 だからです。

Non-static field.png

だったら、

MainActivityをこう修正すればイイじゃない?という安易な気持ちで
public class MainActivity extends AppCompatActivity {

    private static TextView textView;

    // 以下略
}

と修正するのは、否、それはチョット違うんでないかい?と気が咎めるのです。

我ながらひどいコードを書いたもんだ

わざとらしいのですが、まあ見てやってください。でも、こんなんで、Android Studioの例の警告は消えます。

MainActivityDasai.java
public class MainActivityDasai extends AppCompatActivity {

    private MyTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.text_view);
        task = new MyTask();
        task.execute(textView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        task.cancel(true);
    }

    static class MyTask extends AsyncTask<TextView, Object, Void> {
        @Override
        protected Void doInBackground(TextView... textViews) {
            for (int i = 0; i < 100000; i++) {
                if (isCancelled()) {
                    return null; // break;でもいいかも
                }
                try {
                    Thread.sleep(1000);
                    publishProgress(textViews[0], String.valueOf(i));
                } catch (InterruptedException e) {
                    Log.e("MyTask", e.getMessage(), e);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Object... values) {
            ((TextView)values[0]).setText((String)values[1]);
        }
    }
}

私の逡巡が、我が身と心を迷走させております。

  • このMyTaskは、このMainActivityでしか使われないクラスだから、ネストクラスにしたい。
  • しかしstaticネストクラスにせよとIDEが警告を出す。
  • ところがTextTview型のフィールドはstaticフィールドにはしたくない。
  • となると、このMyTaskとこのMainActivityは、別クラスに分けるか。でもそんなことをしたら、今はこのTextTview1個っきりだからまあいいけど、他にもいろんなViewを複数扱うということになると、参照の受け渡しが面倒だ。
  • そしたらやっぱりネストクラスにするか...。

この私の逡巡が、「 では、TextViewをエンクロージングクラスのフィールドにすること自体をやめよう! 」という思いに至って、このようなダサいプログラムになりました。

而して、このダサいプログラムを書いた自分自身を咎めたくなりました。
どこが気にくわないかと言うと、ジェネリクスにObjectを指定する、というところに嫌気がさします。
おかげで、「((TextView)values[0]).setText((String)values[1]);」の部分なんて、キャストしまくり、配列のインデックスどっちがどっち?と惑わせます。
加えて、doInBackgroundメソッドの引数そしてAsyncTaskの第1ジェネリクスをTextViewにしたことも気にくわないです。TextView以外のViewが増えたらどうするんだよ、と。

WeakReferenceを使って、もらったActivityを"弱い参照"で扱う

java.lang.ref.WeakReferenceを使って 弱い参照 として扱うことにしました。前掲のMainActivityGood.javaをご覧ください。

ポイントは、

  • staticネストクラスにしたMyTaskにコンストラクタを定義する。
  • エンクロージングクラスをジェネリックスで指定したWeakReference型フィールドも定義する。
  • そしてそれをコンストラクタの引数を使ってnewする。これにてMainActivityGoodの弱い参照を保持!
  • あとは必要に応じて、WeakReferenceからMainActivityGoodをゲット(メソッド名もget())して使う。

前出のダサいやり方と違って、これならTextView以外のViewを扱うことになっても対応が簡易になりますし、ジェネリクスにObjectを指定するという愚なこともせずにすみます。

以上です。

参考

Qiitaのこの記事「This Handler class should be static or leaks might occur」に近い話です。

AsyncTaskはAPI レベル Rから非推奨になりました

android.os.AsyncTaskは、API レベル R1から非推奨になります。
この記事も、廃れる運命にあるわけです...。

代替策は、

とのことです。後者は、要はKotlinのCoroutinesのことです。


  1. APIレベルは数字で示されるものなのに、この記事執筆時点では「R」となってます。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Theme.AppCompat.Light.NoActionBar を使っているアプリの Toolbar のテキスト、アイコンの色を白くする。

こんばんは。普段業務と個人で Android をゴリゴリ書いています。
今回、Toolbar の icon を白くしようとして少しハマったのでその備忘録です。
かなり限定的な場面で使用するので需要はそんなにないかと思いますが、同じ場面ではまった方の力になれば幸いです。

アプリの設定

AndroidManifest.xml
    <application
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
style.xml
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

上記のコードを見ていただければわかるのですが、ActionBar 無しのテーマを全体に適用し、構成しています。
使用しているのは Theme.AppCompat.Light.NoActionBar ですので、文字の色とアイコンの色は黒くなってしまいます。

結論

結論ですが、 Toolbar に別で作成した Toolbar 用の スタイルを android:theme に設定してあげます。

style.xml
    ...

    <!-- Toolbar のタイトルを白くする -->
    <style name="Toolbar.Light.Text" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
        <item name="android:background">@color/colorPrimary</item>
    </style>
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- style ではなく theme に設定します -->
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Toolbar.Light.Text"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

ポイントは android:style ではなく android:themestyle.xml に作成したスタイルを追加するところです。

android:style に設定した場合は指定した View (今回は ViewGroup)にだけ作用します。
一方、 android:theme に設定した場合は、特に ViewGroup の場合は、その子ビューに対しても作用します。

これでテキストとアイコンは白くなります。

最後に

なんだかAndroidはアプリ内リソースを簡単に管理できるところは良いのですが、ちょっと複雑だなぁと改め思いました。
見ていただきありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む