Android Kotlin日本語チュートリアル-④Android推奨アーキテクチャを取り入れる
Android Kotlin日本語チュートリアル
本連載記事はこれからAndroidアプリ開発を始める人に向けたチュートリアルです。
コンセプトは
- プログラミングをあまり知らない人でも完走できる
- プログラミングにある程度詳しい人にも満足できる
- 実用的な知識を提供する
- とにかくわかりやすく
で、全9回と長めですが頑張っていきましょう。
このチュートリアルを終える頃には、Android開発の土台が形成されているだけでなくアプリケーションアーキテクチャの知識が出来上がっているはずです。
作成するのは以下のようなメモアプリです。
完成品は HiroshiARAKI/AndroidKotlinTutrialで公開していますので適宜参考にしてください。
第4回 : Android推奨アーキテクチャを取り入れる
第4回は、タイトル通りAndroid推奨アーキテクチャについての話です。
今回も解説部分が多くなるところもありますが、結構大事なのでできるだけ飛ばさずに進めてください。
ソフトウェアアーキテクチャについて
ソフトウェア開発、アプリケーション開発ではアーキテクチャという概念は大切です。
有名で最も古いアーキテクチャのMVC (Model View Controller) をはじめ、最近ではMVP (Model View Presenter) や MVVM (Model View ViewModel)、そしてClean Architectureなどが人気です。
このようなソフトウェアアーキテクチャ (アプリケーションアーキテクチャ) で共通している大事な概念は、依存関係を単純に、そして一方通行にすることです。
「何言ってるかわからん」という方もいるかと思いますが、このまま読み進めてください。
Android推奨アーキテクチャ
アプリ アーキテクチャ ガイド | Android developerによると、Androidでは以下のようなアーキテクチャでアプリを設計することが推奨されています。
MVVMと少し似ていますが厳密には違います。
大事なのは、各要素でしっかりと責務を分けることです。
ActivityとFragmentはわかると思いますが、他の要素はまだ紹介していないのでこれから紹介します。
- Activity / Fragment … Viewに関する簡単な操作のみを扱う。
- ViewModel … View生成及び操作に必要なロジックや、LiveDataを管理する。基本的に各Activity/Fragmentに対して一つだけ存在する。
- Repository … データとの境界面であり、データ取得に必要なロジックを管理する。
Modelの層は一旦置いておいて、このような役割を各要素に持たせるようにします。
もっと噛み砕いて言うと、「Repositoryは上位のViewModelやActivityの存在を知らなくても動作するべき」または「ViewModelはActivityを知らなくても動作するべき」と表現できます。
もう一つ具体的な例を挙げると、「ActivityはViewModelのインスタンス(参照)を持っていても良いが、ViewModelはActivityのインスタンス(参照)を持つべきではない」とも言えます。
少しややこしいですかね。
今はわからなくてもこれを意識してプログラミングすることで、アプリの管理が何倍にもしやすくなります。
ViewModelを作ってみる
それでは早速ViewModelを作ってみましょう。
新たに viewmodel/
パッケージを作成し、MainActivityのViewModelとして、MainViewModel.ktを作成します。
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel() {
}
とりあえず、ViewModel()
を継承させれば、それだけでViewModelにはなります。
そうしたら、ViewModelを簡単に扱うために新しく Android Jetpackの必要となるライブラリをGradleに追加しましょう。
build.gradle (Module)を開いて以下を加筆してください。
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
これでViewModelの扱いが少しだけ楽になります。
この拡張ライブラリで以下のようにViewModelの初期化をシンプルに記述可能です。
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
// ...
}
}
これからは複雑なロジックを含む操作は全てViewModelに任せる、という風にしましょう。
LiveDataを使ってデータを監視する
AndroidアーキテクチャやMVVMで扱うViewModelは、大きな責務のひとつとしてData Bindingがあります。
簡単に説明すると、「データを監視し続け、データ変更の変更をトリガーになにかしらの処理を開始する」のがData Bindingです。
実際に実装しながら、その様子を確認しましょう。
まずはMainViewModelに、RecyclerViewのデータを管理するLiveDataを作ります。
class MainViewModel : ViewModel() {
/** ViewModel内で扱うミュータブルなLiveData */
private val _memoItems = MutableLiveData<List<Memo>>()
/** 外部公開用のイミュータブルなLiveData */
val memoItems: LiveData<List<Memo>> = _memoItems
}
ちなみにミュータブル (Mutable)/イミュータブル (Immutable)について補足が必要であれば KotlinのMutableとImmutable【初学者向け】 を参照してください。
そうしたらMainActivity側で、このLiveDataを監視し、変更があったら何をするかを決めましょう。
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val addButton: Button = findViewById(R.id.add_button)
val recyclerView: RecyclerView = findViewById(R.id.main_list)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MainAdapter(Memo.createFakes())
recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
addButton.setOnClickListener {
supportFragmentManager.beginTransaction().run {
add(R.id.main_container, NewMemoFragment())
addToBackStack(null)
commit()
}
}
viewModel.memoItems.observe(this) {
// ここでRecyclerViewのデータをアップデートしたい
}
}
}
LiveDataのObserve処理を実装する
アプリの理想としては、起動からUI表示は迅速にすべきです。
現在の実装だと、MainAdapter(Memo.createFakes())
でMemoのリスト生成 (最終的にはデータベースやキャッシュからの取得)に時間がかかれば、UIがユーザの目の前に現れるまで時間を要してしまいます。
これは避けるべきで、私たちがやるべきことは一旦空のリストでUIを描画してデータが定まり次第、再描画をかけることです。
早速それをやってみます。
MainAdapter.kt
class MainAdapter : RecyclerView.Adapter<MainAdapter.MainViewHolder>() {
private var memoItems: List<Memo> = emptyList()
// ...
/** データの変更とデータ変更通知 */
fun setMemoItems(items: List) {
memoItems = items
notifyDataSetChanged()
}
}
MainActivity.kt
val recyclerView: RecyclerView = findViewById(R.id.main_list)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MainAdapter()
recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
これで空リストでのUI描画がまずは行われます。
そうしたらViewModel側でデータの読み込みを行います。
MainViewModel.kt
class MainViewModel : ViewModel() {
/** ViewModel内で扱うミュータブルなLiveData */
private val _memoItems = MutableLiveData<List<Memo>>()
/** 外部公開用のイミュータブルなLiveData */
val memoItems: LiveData<List<Memo>> = _memoItems
/** メモリストを読み込む */
fun loadMemoItems() {
// データの取得は非同期で
viewModelScope.launch(Dispatchers.IO) { // データ取得はIOスレッドで
// LiveDataのメインスレッド以外での変更は、postValue()を使う
// メインスレッドならば、_memoItems.value = でOK
_memoItems.postValue(Memo.createFakes()) // 本来はDBやCacheから取得
}
}
}
あとは、MainActivityでデータの読み込みと、データ変更時の処理を追記するだけです。
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val addButton: Button = findViewById(R.id.add_button)
val recyclerView: RecyclerView = findViewById(R.id.main_list)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MainAdapter()
recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
addButton.setOnClickListener {
supportFragmentManager.beginTransaction().run {
add(R.id.main_container, NewMemoFragment())
addToBackStack(null)
commit()
}
}
viewModel.memoItems.observe(this) {
(recyclerView.adapter as MainAdapter).setMemoItems(it)
}
viewModel.loadMemoItems()
}
おそらく、エミュレータでの動作自体はほとんど変わらないはずですが、空リストでの生成→データ読み込み→再描画が行われているはずです。
試しにMainViewModel#loadMemoItems()
を以下のように変更してみると、3秒後に再描画が行われ、この処理がわかりやすくなるでしょう。
/** メモリストを読み込む */
fun loadMemoItems() {
viewModelScope.launch(Dispatchers.IO) {
delay(3000) // 3秒 = 3000ms待機
_memoItems.postValue(Memo.createFakes())
}
}
補足: launchによる非同期処理
Kotlinでは、誰かのLifecycleScopeを基準に非同期処理を開始 (launch) できます。
Android開発では以下のことを守りましょう。
GlobalScope
は基本使用しない- データの取得は
Dispatchers.IO
を指定する
これらを守るだけでも十分良いAndroid開発ができます。
詳しくはAndroid Kotlinにおける非同期処理Tipsを参照してください。
第4回のまとめ
- Android推奨アーキテクチャはMVVMをベースにしたもの
- 各クラスで責務を明確に分ける
- 下層クラスは上層クラスの存在に依存しないようにする
- ViewModelではLiveDataや比較的複雑なロジックを管理する
- ViewModelを管理するActivityやFragmentでLiveDataを監視 (Observe)し、データ変更時の処理を記述できる
- アプリ起動からUI描画は迅速に行うようにする
おわりに
第4回はここで終わりになります。
今回も盛りだくさんな内容でしたが、一番大事なのは依存関係を簡易にすることで、Activity (Fragment)→ViewModelという単一方向の依存関係を守りましょう。
そしてViewModelからActivity (Fragment)にデータ変更を通知するには、Activityのメソッドを呼ぶのではなく、LiveDataの監視によって実現することで、双方向の依存では無くすことができます。
まだいまいち理解が追いついていない方もいるかもしれませんが、とにかくViewModelで管理側ActivityやFragmentの参照 (インスタンス) を持ったらNGということだけ頭に入れておいてください。
これは次に解説する、ViewModel→Repositoryでも同様です。