2020/03/11

by ViewModels() を使って ViewModel を取得する方法

以前は以下のように [ViewModelProviders](https://developer.android.com/reference/androidx/lifecycle/ViewModelProviders) を使用して [ViewModel](https://developer.android.com/reference/androidx/lifecycle/ViewModel) を取得していましたが、この方法は Deprecated になりました。

```kotlin
val myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
```

ドキュメントを見てみると、代わりに kotlin の委譲 `by viewModels()`、もしくは `ViewModelProvider`使い方は最後に参照として書きました を使ってね。と書いてあります。

ところが、`by viewModels()` はどう使うのかが、何処にも書いてありません少なくとも2020年3月10日時点で公式のドキュメントはなさそう。

そこで、ネットの情報とコードを参考に使ってみました。



### 依存関係
`viewModels` は `fragment-ktx` というパッケージ内に入っているのでこれも公式には書いてなさそう、これを `implementation` に加えます。

```groovy
`title: "build.gradle in app";
dependencies {
    ...
    implementation 'androidx.fragment:fragment-ktx:1.2.2'
}
```

### コンパイラのバージョンを 1.8 互換に
環境によっては以下のように、JVM のターゲットバージョンが合ってない旨のエラーがでてしまいます。

"Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option"

これを解決するには、以下のように `build.gradle` に追記します。

```groovy
`title: "build.gradle in app";
android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}
```

この問題、`viewModels()` に限ったことではなく、新しめの kotlin ライブラリを使おうとすると起こります。

> 参考
>
> [android - Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6 - Stack Overflow](https://stackoverflow.com/a/56996020)


### 基本の使い方
以下のように使用します。
```kotlin
class MainActivity : AppCompatActivity() {
    // lazy なので最初に使用する時に初期化される
    private val myViewModel by viewModels<MyViewModel>()

    fun something() {
        myViewModel.users.observe(this, Observer {
            // Do something
        })
    }
}
```

### Factory 指定
Factory を指定した初期化の方法は以下。
```kotlin
private val myViewModel  by viewModels<MyViewModel>{ MyViewModelFactory() }
```

`viewModels` の実装を覗いてみると、以下のようになっています。
`Factory` を返す関数を与えられるようになっているわけですね。
```kotlin
@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
    noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }

    return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
```

`ViewModelLazy()` も public なクラスなので、これを直接使えば `ViewModelStoreOwner` (Activity や Fragment 等)を指定できます。
しかし、これは遅延初期化オブジェクト。どのタイミングで呼ばれるかが定かではありません正確には、呼び出しを全部網羅して確認すれば不可能ではないけど、それだと lazy を使う意味はもうない。
Activity や Fragment 等、ライフサイクルによっては正しく動作しないものを指定すべきではありません。

そういった用途には以下の `ViewModelProvider` を使うのが良いでしょう。

### 【参考】ViewModelProvider クラスを使う
ドキュメントには `by viewModels()` もしくは [ViewModelProvider()](https://developer.android.com/reference/androidx/lifecycle/ViewModelProvider) を使って。とかいてあります。
こちらは、`ViewModelProviders.of()` とほぼ同様なので、簡単に使えるかと思います。

```kotlin
// ViewModelProviders.of(this) とほぼ同じように使える
val myViewModel= ViewModelProvider(this).get(MyViewModel::class.java)
```

0 件のコメント: