Seize the day

POST : Android Dev Study

DataBinding ViewHolder 새로 만들지 않고 사용하기

코드를 중복시키는 것을 싫어해서 예전에 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
}

 

top

posted at

2020. 8. 2. 02:57


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크