初めに
社内でAndroidの勉強会を開催する機会があり、記事を書こうと思いました。
今回は「AndroidArchitectureComponents」を使ってMVVMを実現する(実装する)内容となっています。別記事で、MVVMの説明した記事を書いてます。
※内容がおろそかだったらごめんなさい
今回作るもの
以下を行う双方向データバインディングアプリ
- 画面を開いた時に、Modelからデータを取得しViewに変更通知
- 画面から文字を入力し、Viewに変更を通知
設計イメージ
実装手順
Model実装 → ViewModel実装 → View実装
さぁ実装!の前に
実装の前に、知っていただきたいことがあります。
今回使用するDataBinding、LiveData、ViewModelをざっくり知っていただこうと思います。
それを知らないと何やっているかわからないと思うからです。ちなみに、別記事で詳しく述べる予定です。
DataBindingとは
Viewとデータ情報を静的または動的に結合する技術
####単方向その1
ViewModelの値を変更する -> Viewに自動で反映される
####単方向その2
ユーザがViewに入力 -> 自動でViewModelに値がセットされる
####双方向
ViewModelの値を変更する -> Viewに自動で反映される
ユーザがViewに入力 -> 自動でViewModelに値がセットされる
LiveDataとは
値の変更をObserveできるデータホルダー
LiveData
外部から変更不可なLiveData
MutableLiveData ← 今回はこれだけ知っていればいい!
外部から変更可能なLiveData
MediatorLiveData
複数のLiveDataを束ねて管理するMutableLiveData
ViewModelとは
Activityの画⾯回転時のデータ保持
Activityの複数Fragment間でのデータ受け渡し
LiveDataと併⽤することが多い ← 今回はこれだけ知っていればいい!
プロセス停⽌後は復旧できない
データの永続化ではない
ViewModelProviders.of()とViewModelProvider.get()を使い、⾃分でnewしない! ← あ、これも知ってて!
実装
Gradleの設定
apply plugin: 'kotlin-kapt'
android {
dataBinding {
enabled = true
}
}
dependencies {
def archComponents_version = '2.0.0-beta01'
implementation "androidx.lifecycle:lifecycle-extensions:$archComponents_version"
kapt "androidx.lifecycle:lifecycle-compiler:$archComponents_version"
}
Model(Repository)
シンプルな初期値のみを取得する
class MainRepository {
fun fetchText(): String {
return "初期値!!"
}
}
ViewModel
変更可能なLiveDataにModelから取得した情報を格納する
class MainViewModel : ViewModel() {
var liveDataText: MutableLiveData<String> = MutableLiveData()
fun fetchText() {
liveDataText.value = MainRepository().fetchText()
}
}
レイアウト
xmlファイルのルートをにして、使用するオブジェクトを定義すると、Viewを実装
Viewからバインドしたdataにアクセスするには、@{}
でくくる
また、@{}
はnullを許容するようになっており、NullPointerExceptionは発生しない
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="view_model" type="com.example.mvvmstudy.viewmodel.MainViewModel"/>
<import type="java.lang.Integer" />
<import type="android.view.View"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="@={view_model.liveDataText}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/count_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{`文字数:` + Integer.toString(view_model.liveDataText.length())}"
app:layout_constraintTop_toBottomOf="@+id/edit_text"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
View(ActivityとFragment)
ActivityはFragmentを呼ぶだけ
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(R.id.container, MainFragment())
.commit()
}
}
}
-
<layout>
でくくったレイアウトファイルがあると、自動的にxmlファイル名に応じたBindingクラスが作られます。 -
DataBindingUtil.inflate
でBindingしたViewを取得する。 -
binding.viewModel = viewModel
でレイアウトファイルに定義したViewModelにViewModelのインスタンスを格納。 - ViewModelのLiveDataをオブザーブし、データに変更があったらテキストを変更するようにする。
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBindingBinding
private val viewModel: MainViewModel by lazy {
ViewModelProviders.of(this).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.liveDataText.observe(this, Observer {
binding.countText.text = "文字数:" + it.length.toString()
})
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main_binding, container, false)
binding.viewModel = viewModel
viewModel.fetchText()
return binding.root
}
}
しかし、これでは単方向データバインディング。
- 双方向データバインディングするために、
binding.lifecycleOwner = viewLifecycleOwner
を設定する。 - オブザーブしていた処理は不要になったため、削除
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBindingBinding
private val viewModel: MainViewModel by lazy {
ViewModelProviders.of(this).get(MainViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main_binding, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
viewModel.fetchText()
return binding.root
}
}
以上。
最後に
基本的な「MVVM + LiveData + DataBinding」を実装しました。
最終的に双方向にしたので、LiveDataは関係なくなってしまいましたが。。。
また、ViewModelからRepositoryを直接読んでいますが、これでは依存しているため、
RepositoryのInterfaceを作り、ViewModelのコンストラクターでそのRepositoryのIntefaceを渡すようにするとなお良くなる。
###GitHub
勉強会のカンペ用に色々無駄なコードが書かれちゃっているけど
https://github.com/mk2taiga/MVVMStudy
参考にした記事
https://qiita.com/Omoti/items/a83910a990e64f4dbdf1
https://qiita.com/takaaki7/items/91d34e8bf9ad5d71ddd2