はじめに
DataBindingとListViewを使って簡単なリスト表示をやってみたときのメモ。
やったこと
1. DataBindingの有効化
Gradle(app)に設定を追記。
android {
dataBinding {
enabled true
}
}
2. Modelの作成
ListViewに表示するデータを保持、整形するためのModelを作成。
class Person(
private val firstName: String,
private val lastName: String,
private val age: Int
) {
fun getFullName() = "$firstName $lastName"
fun getDisplayAge() = "$age 歳"
}
3. ViewModelの作成
ModelとViewをつなぎ合わせるためのViewModelを作成。
ほとんどの属性値、メソッドはビューとのバインド用。
class MainViewModel : ViewModel() {
private val dataList by lazy { ArrayList<Person>() }
private val _items = MutableLiveData<List<Person>>()
val items: LiveData<List<Person>> = _items
var firstName = MutableLiveData("")
var lastName = MutableLiveData("")
var age = MutableLiveData("")
var isEnabled = MediatorLiveData<Boolean>()
private fun isAllEntered() =
firstName.value!!.isNotBlank() &&
lastName.value!!.isNotBlank() &&
age.value!!.isNotBlank()
init {
isEnabled.addSource(firstName) { isEnabled.postValue(isAllEntered()) }
isEnabled.addSource(lastName) { isEnabled.postValue(isAllEntered()) }
isEnabled.addSource(age) { isEnabled.postValue(isAllEntered()) }
}
fun onAddClick() {
val fnVal = firstName.value ?: ""
val lnVal = lastName.value ?: ""
val ageVal = if (age.value!!.isEmpty()) 0 else Integer.parseInt(age.value!!)
dataList.add(Person(fnVal, lnVal, ageVal))
_items.value = dataList
}
fun getOnItemClickListener(): AdapterView.OnItemClickListener {
return AdapterView.OnItemClickListener { adapterView, view, i, l ->
dataList.removeAt(i)
_items.value = dataList
}
}
}
4. カスタムバインディングアダプタの作成
カスタムバインディングアダプタを作成することで、
レイアウトファイルに対してカスタム属性とメソッドをバインドさせることができる。
@BindingAdapter("onItemClickListener")
fun setOnItemClickListener(listView: ListView, listener: AdapterView.OnItemClickListener) {
listView.onItemClickListener = listener
}
メソッドに対して@BindingAdapter(属性)
を付与することによって
レイアウトファイルに任意の属性が設定でき、バインドができる。
<ListView
app:onItemClickListener="@{viewModel.getOnItemClickListener()}">
</ListView>
属性値であるviewModel.getOnItemClickListener()
が
setOnItemClickListener
の第2引数に渡される。
ここでは、ListViewに対してクリック時のリスナーを登録。
5. レイアウトファイルの作成
例によってデータバインディング用のレイアウトに変換したのち、
バインドさせたい属性とViewModelの属性値を
@{~}
(単方向)、@={~}
(双方向)でバインドさせていく。
・メインアクティビティ
<?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="viewModel"
type="com.example.mymvvmlistview.viewmodel.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/etLastName"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3.0"
android:text="@={viewModel.lastName}"
android:ems="10"
android:hint="苗字"
android:inputType="textPersonName"
android:autofillHints="" />
<EditText
android:id="@+id/etFirstName"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3.0"
android:text="@={viewModel.firstName}"
android:ems="10"
android:hint="名前"
android:inputType="textPersonName"
android:autofillHints="" />
<EditText
android:id="@+id/editText3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:text="@={viewModel.age}"
android:ems="10"
android:hint="年齢"
android:inputType="number"
android:autofillHints="" />
<Button
android:id="@+id/btAdd"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:onClick="@{() -> viewModel.onAddClick()}"
android:text="追加"
android:enabled="@{viewModel.isEnabled()}"/>
</LinearLayout>
<ListView
android:id="@+id/lvItem"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:onItemClickListener="@{viewModel.getOnItemClickListener()}">
</ListView>
</LinearLayout>
</layout>
・ListView用のカスタムセル
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="person"
type="com.example.mymvvmlistview.model.Person" />
<variable
name="viewModel"
type="com.example.mymvvmlistview.viewmodel.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:id="@+id/tvName"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="30dp"
android:layout_weight="0.7"
android:gravity="center"
android:textAlignment="center"
android:textSize="18sp"
tools:text="@{person.fullName}" />
<TextView
android:id="@+id/tvAge"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="30dp"
android:layout_weight="0.3"
android:gravity="center"
android:textAlignment="center"
android:textSize="18sp"
tools:text="@{person.displayAge}" />
</LinearLayout>
</layout>
6. ListViewのアダプタを作成
ListViewで使用するカスタムアダプタを作成する。
class MyCustomAdapter(
private val context: Context,
private var dataList: List<Person>
) : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val binding =
if (convertView == null) {
val inflater = LayoutInflater.from(context)
// https://developer.android.com/topic/libraries/data-binding/expressions?hl=ja
val tBinding: ListItemCellBinding =
ListItemCellBinding.inflate(inflater, parent, false)
// tagにインスタンスをセット(convertViewが存在する場合に使い回すため)
tBinding.root.tag = tBinding
tBinding
} else {
convertView.tag as ListItemCellBinding
}
binding.person = getItem(position) as Person
// 即時バインド
binding.executePendingBindings()
return binding.root
}
fun updateItems(newItems: List<Person>) {
dataList = newItems
// 変更の通知
notifyDataSetChanged()
}
override fun getItem(position: Int): Any {
return dataList[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getCount(): Int {
return dataList.size
}
}
7. Activityの修正
class MainActivity : AppCompatActivity() {
private val mViewModel: MainViewModel by lazy {
ViewModelProvider(this).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this, R.layout.activity_main
)
with(binding) {
viewModel = mViewModel
lvItem.adapter = MyCustomAdapter(applicationContext, ArrayList(0))
lifecycleOwner = this@MainActivity
(viewModel as MainViewModel).items.observe(this@MainActivity, Observer { item ->
val adapter = lvItem.adapter as MyCustomAdapter
adapter.updateItems(item)
})
}
}
}
8. 動作確認
双方向バインディングにより、画面上の「苗字」「名前」「年齢」の値を監視し、
なにかしらの入力値が入るまで追加ボタンを押すことができないようにした。



参考
先人の知恵をお借りしました。
ありがとうございます。
Android DataBinding 〜ListViewで使う〜
【Android】LiveData+DataBinding+ViewModelでListView作成
How can I use Android DataBinding in a listview and still use a ViewHolder pattern?