코드를 중복시키는 것을 싫어해서 예전에 RecyclerViewAdapter도 하나의 구현 코드만 사용하고 ViewHolderCreator interface를 이용해서 ViewHolder만 새로 만들어사용했는데 DataBinding을 이용하면 ViewHolder도 만들지 않아도 될 것 같아서 이렇게 샘플 코드를 작성해 보았다.
설치된 app 목록을 보여주고, 선택된 앱 목록을 저장하고, 이름으로 필터하는 간단한 프로그램인데 전체 코드는 아래와 같다.
AppListActivity
class AppListActivity : BaseAppCompatActivity(R.layout.app_list, R.menu.app_list) {
private val adapter: DataBindingRecyclerAdapter = DataBindingRecyclerAdapter(ViewModel())
private val recyclerView: RecyclerView by findView(R.id.recycler_view)
private val progressBarView: View by findView(R.id.progress_bar)
private val selectedApps = HashSet<String>()
private lateinit var appItemList: List<AppItem>
private var searchWord: String? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
recyclerView.adapter = adapter
Settings.pushNotiApps.value.split('|').filter { it.isNotEmpty() }.forEach {
selectedApps += it
}
loadList()
}
private fun loadList() = lifecycleScope.launch {
progressBarView.isVisible = true
appItemList = withContext(Dispatchers.IO) {
val pm = packageManager
pm.getInstalledApplications(PackageManager.GET_META_DATA)
.filter { (it.flags and ApplicationInfo.FLAG_SYSTEM) == 0 } // except system app
.map { applicationInfo ->
AppItem(
applicationInfo.loadIcon(pm),
applicationInfo.loadLabel(pm).toString(),
applicationInfo.packageName,
selectedApps.contains(applicationInfo.packageName)
)
}
.sortedWith(compareBy({ !it.selected }, { it.name })) // order by selected, name
.toList()
}
adapter.clear()
adapter.addAll(appItemList)
progressBarView.isVisible = false
}
@OnMenuClick(R.id.menu_save)
fun onClickSave(menuItem: MenuItem) {
if (!::appItemList.isInitialized) return
appItemList
.filter { it.selected }
.joinToString("|") { it.packageName }
.also {
Settings.pushNotiApps.value = it
}
finish()
Utils.startNotiListenService(this)
}
@OnMenuClick(R.id.menu_search)
fun onClickSearch(menuItem: MenuItem) {
if (!::appItemList.isInitialized) return
InputDialogBuilder(this)
.setMaxLines(1)
.setTitle(R.string.search)
.setValue(searchWord)
.setValueCanEmpty(true)
.setCallback2 { dialog, value ->
searchWord = value.takeIf { it.isNotEmpty() }
appItemList
.filter { appItem ->
searchWord?.let { appItem.name.contains(it, ignoreCase = true) } ?: true
}
.also {
adapter.clear()
adapter.addAll(it)
}
dialog.dismiss()
}
.show()
}
class AppItem(val icon: Drawable, val name: String, val packageName: String, var selected: Boolean) :
RecyclerViewItem {
val backgroundColor: Int
get() {
return if (selected) Color.GREEN else Color.TRANSPARENT
}
override fun getLayoutId(): Int = R.layout.app_list_item
}
inner class ViewModel : DataBindingViewModel {
fun onClick(item: AppItem) {
item.selected = !item.selected
adapter.notifyItemChanged(adapter.getPosition(item))
}
}
}
R.layout.app_list
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
R.layout.app_list_item
<?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="vm"
type="com.mdiwebma.leetzsche.activity.noti.AppListActivity.ViewModel" />
<variable
name="item"
type="com.mdiwebma.leetzsche.activity.noti.AppListActivity.AppItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@{item.backgroundColor}"
android:onClick="@{(v) -> vm.onClick(item)}">
<ImageView
android:id="@+id/icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="10dp"
android:src="@{item.icon}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="3dp"
android:text="@{item.name}"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/package_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="3dp"
android:text="@{item.packageName}"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toBottomOf="@+id/name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
실제로 구현한 것은 AppItem과 ViewModel이 전부고, 나머지 코드는 공용 코드라고 생각하면 된다. 다른 페이지나 프래그먼트에서 수정없이 재사용되는 코드이다.
공용코드
interface DataBindingViewModel
interface RecyclerViewItem {
fun getLayoutId(): Int
}
open class DataBindingViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
fun onBind(vm: DataBindingViewModel?, item: RecyclerViewItem) {
if (vm != null) binding.setVariable(BR.vm, vm)
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
}
class DataBindingRecyclerAdapter(
private val viewModel: DataBindingViewModel? = null
) : RecyclerView.Adapter<DataBindingViewHolder>() {
private val itemList = mutableListOf<RecyclerViewItem>()
private var notifyOnChange: Boolean = true
private val lock = Any()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder {
return DataBindingViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
viewType,
parent,
false
)
)
}
override fun getItemCount(): Int = itemList.size
override fun getItemViewType(position: Int): Int = itemList[position].getLayoutId()
override fun onBindViewHolder(holder: DataBindingViewHolder, position: Int) =
holder.onBind(viewModel, itemList[position])
fun getItemList(): List<RecyclerViewItem> = itemList
fun getPosition(item: RecyclerViewItem): Int = itemList.indexOf(item)
fun add(item: RecyclerViewItem) {
synchronized(lock) {
itemList.add(item)
}
if (notifyOnChange) notifyDataSetChanged()
}
fun add(index: Int, item: RecyclerViewItem) {
synchronized(lock) {
itemList.add(index, item)
}
if (notifyOnChange) notifyDataSetChanged()
}
fun add(vararg items: RecyclerViewItem) {
synchronized(lock) {
itemList.addAll(items)
}
if (notifyOnChange) notifyDataSetChanged()
}
fun addAll(list: Collection<RecyclerViewItem>) {
synchronized(lock) {
itemList.addAll(list)
}
if (notifyOnChange) notifyDataSetChanged()
}
fun remove(item: RecyclerViewItem) {
synchronized(lock) {
itemList.remove(item)
}
if (notifyOnChange) notifyDataSetChanged()
}
fun clear() {
synchronized(lock) {
itemList.clear()
}
if (notifyOnChange) notifyDataSetChanged()
}
fun setNotifyOnChange(notifyOnChange: Boolean) {
this.notifyOnChange = notifyOnChange
}
}
여기서 좀 더 보강한다면 ViewHolderCreator를 제공한다던지, 더 보기 로딩뷰 같은 것을 구현하면 된다.
interface DataBindingViewHolderCreator {
fun createViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder
}