1
0

More than 3 years have passed since last update.

Sunflowerリポジトリで学ぶJetPack〜ViewModel編

Last updated at Posted at 2020-07-26

新型コロナの影響で自宅待機になってしまい、その間勉強するものとしてSunflowerリポジトリを
勧めてもらいました。

JetPackのライブラリのうち、今回はViewModel編です
DataBinding編LiveData編などでも、
チラチラ出てきてはいましたが、見てみぬふりをしていました笑

尚、引用しているソースは明記しているところ以外は、基本的には全てSunflowerのリポジトリのものです。 

環境

  • 確認時はAndroid Studioのバージョンは 3.6.2を使用しました
  • JetPackAndroidXライブラリを利用するのでCompile SDK28以上にする必要があります

そもそもViewModelってなに?

公式の説明によると

  • ViewModel は、ライフサイクルを意識した方法で UI 関連のデータを保存および管理するためのクラスです
  • ViewModel クラスを使用すると、画面の回転などの設定の変更後にデータを引き継ぐことができます。

です!

ViewModelってなに?(詳細)

公式にはこんな感じのことが書いてありました。

  • UI コントローラで利用するデータをバンドルから復元して利用しないで済む
    • アクティビティがonSaveInstanceState() メソッドを使用して onCreate() のバンドルからデータを復元しなくてもよくなります。
    • そもそもこの方法(onSaveInstanceState()利用)が適しているのは少量のデータの場合だけです。
    • ユーザーやビットマップのリストのようにデータの量が多くなる可能性がある場合には適していません。

画面をぐるぐるする度に、バンドルからデータをとって。。。とするのはめんどくさかったので、
それをしないで済むのは良さそうですね!

  • メモリリークの心配がない
    • UI コントローラでは実行に時間がかかる非同期呼び出しを頻繁に行う必要があります。それらを管理し、破棄された後にシステムが呼び出しのクリーンアップを行ってメモリリークが発生しないようにする必要がありますが、ViewModelを利用した場合それをする必要がありません。

画面のライフサイクルに合わせて、データの破棄や再取得を考えて設計・実装するのは大変ですね。。。
それをしないで済むのはこちらも良さそうです!

  • リソースの無駄がない
    • この管理ではメンテナンスを何度も実施する必要があります
    • 設定の変更によってオブジェクトが再作成された場合にはすでに行った呼び出しを再度行わなければならないこともあるため、リソースが無駄になります。

メモリリークしない作りにするために、冗長な処理になってしまいがちだったかもしれません。
それをしないで済むのはこちらもありがたいです!

  • UIコントローラクラスの肥大化防止とテスト効率の向上
    • UI コントローラに対してデータベースやネットワークからのデータ読み込みも行うよう要求すると、クラスが肥大化することになります
    • UI コントローラに過度の役割を割り当てると、アプリの作業を他のクラスに任せずに 1 つのクラスですべて処理しようとすることになり、テストも困難になります。
    • ビューデータの所有権を UI コントローラのロジックから切り離すことで、複雑さが軽減され、効率性が高まります。

処理が増えてくると、ActivityFragmentが巨大化してしまい、本当のところ実際にUIをコントロールしているところが
見えにくくなってしまう、という状況になってしまいがちだと思います。
また、処理とUIコントロールの部分が分割できるよう、設計を工夫してなんとかしていたと思いますが、
公式でサポートするViewModelで出来るのは頼もしいですね!!!

ViewModelクラス

その名の通り、上記のようなUIコントローラー向けにUIデータを準備するためのViewModelヘルパークラスが用意されています!!!
それを使えば上記の問題が解決できるはずです!

使用箇所

Sunflowerリポジトリの中ではどのようにViewModelが使われているか、実際に見てみましょう

ViewModelの定義

GardenPlantingListViewModel.kt
class GardenPlantingListViewModel internal constructor(
    gardenPlantingRepository: GardenPlantingRepository
) : ViewModel() {
    val plantAndGardenPlantings: LiveData<List<PlantAndGardenPlantings>> =
            gardenPlantingRepository.getPlantedGardens()
}
  • plantAndGardenPlantingsを取得する処理は、アクティビティやフラグメントではなく、GardenPlantingListViewModel内に割り当てられてられています。
  • ViewModel内で定義しているデータはLiveDataとなっています

    • こうすると更新を監視できて、非アクティブになった時に破棄できるなどメリットがあるため、一緒に使う場合が多いと思います。
    • ※詳しくはLiveData編も併せて確認してください。
  • ちなみに、ここではplantAndGardenPlantingsの値自体の取得処理は、GardenPlantingRepositoryを利用し抽象化されており、さらに外部でインスタンス化されたものをコンストラクターで渡されています。(依存性注入)

    • こうすることで、取得先がローカルDBからサーバーに変わったとしても、このクラスとの依存関係がないため、変更は不要であり、変更に強い設計になっています。
    • 尚、依存性注入についてはDagger2を利用して実施する場合が多いですが、Sunflowerリポジトリでは、Dagger2の依存性注入については、対応しない!と明言しています笑

ViewModel利用箇所

UI コントローラー(ここではFragment)ではこのようにして、ViewModelを利用していました。

GardenFragment.kt
class GardenFragment : Fragment() {
          :
          :
    private val viewModel: GardenPlantingListViewModel by viewModels {
        InjectorUtils.provideGardenPlantingListViewModelFactory(requireContext())
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentGardenBinding.inflate(inflater, container, false)
          :
          :
        subscribeUi(adapter, binding)
        return binding.root
    }

    private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) {
    viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result ->
        binding.hasPlantings = !result.isNullOrEmpty()
        adapter.submitList(result)
    }
  • viewModelを定義している部分ですが、by viewModels {ラムダ式}となっています。
    • これはAndroid KTXの機能のうち、Properties Delegeteというもので、この場合は下記のパターンです。
    • →ラムダ式の中に自作のFactory Methodを書くことができ、そこで作られたインスタンスをViewModelに与えることができます
class MyFragment : Fragment() {
    val viewmodel: MYViewModel by viewmodels { myFactory }
}

ここでは、InjectorUtils#provideGardenPlantingListViewModelFactory()メソッドを実施し、このViewModelのインスタンスを取得しています

  • viewModel.plantAndGardenPlantingsLiveDataのため、observe()して変更を監視します。
  • この処理はFragmentが作成された時にコールされる処理、onCreateView()メソッド内で実施しています。
  • 値がセットされる度にonChange()メソッドがコールされ、ラムダ式内の処理が実施されます。そこで、UIの更新を実施しています。

ViewModelFactoryメソッドについて

ViewModelFactoryメソッドにはandroidx.lifecycle.ViewModelProviderを使用します。

引数無しの場合

val viewmodel: MYViewModel by viewmodels{ 
    ViewModelProvider.NewInstanceFactory().create(MYViewModel::class.java)
}

これでOKです。(新たなクラスの定義は不要です。)

引数ありの場合(今回のパターン)

ただし、今回は引数がある場合なので、
引数があるViewModelFactoryクラスを作成しています。

こちらはViewModelFactoryクラスのFactoryメソッド(Factoryクラスのコンストラクターメソッド)を呼び出している処理です。
Sunflowerリポジトリでは、kotlin:InjectorUtilsクラスに各ViewModelFactoryメソッドをまとめて記載してあります。

InjectorUtils.kt
object InjectorUtils {
                :
                :
    fun provideGardenPlantingListViewModelFactory(
        context: Context
    ): GardenPlantingListViewModelFactory {
        val repository = getGardenPlantingRepository(context)
        return GardenPlantingListViewModelFactory(repository)
    }
                :
                :
}

続いて、GardenPlantingListViewModelFactoryクラスであるkotlin:GardenPlantingListViewModelFactoryを見てみましょう

GardenPlantingListViewModelFactory.kt
class GardenPlantingListViewModelFactory(
    private val repository: GardenPlantingRepository
) : ViewModelProvider.NewInstanceFactory() {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GardenPlantingListViewModel(repository) as T
    }
}

GardenPlantingRepositoryを引数として渡す形で定義されています。

Sunflowerリポジトリの他のViewModelクラスも同じ形式となっていたので、別途作成する場合もこの形式を踏襲し、以下の部分の変更のみすればよさそうです

  • クラス名(◯◯◯Factory)
  • Constructorの引数
  • オーバーライドするcreateメソッドが返す引数(新たに作成したViewModelのクラス名)

ViewModelとテストについて

  • ViewModelのライフサイクルは長めに設計されています。そのため、ViewModelのテストをしやすい設計となっています。

viewmodel-lifecycle.png

  • 画面が無くなった(Activity#onDestroy())「後」に、なにかやる、などもViewmodelScope.onCleared()とかでできます。
  • この図はActivityについて書かれてますが、Fragmentなどでも基本的には同じのようです

まとめ

  • ViewModel は、ライフサイクルを意識した方法で UI 関連のデータを保存および管理するためのクラスで、画面の回転などの設定の変更後にデータを引き継ぐことができる。
  • UIコントローラー(Activity,Fragmentなど)とライフサイクルを共にするため、メモリリークの心配やリソースの無駄がない
  • UIコントローラーからViewModelを切り出すことにより、UIコントローラーの肥大化を防止し、テストをしやすくすることができる
  • Android KTXProperties DelegeteFactoryメソッドでインスンタンスを作ることができる。
  • ViewModelクラスのコンストラクタが、引数なしの場合は自前のFactoryクラスを作らないでもよいが、引数ありの場合はFactoryクラスを作る必要がある。

以上です!

参考サイト

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0