diff --git a/base/src/main/java/com/bt/base/ui/BaseFragment.kt b/base/src/main/java/com/bt/base/ui/BaseFragment.kt index a3445b0..6175ced 100644 --- a/base/src/main/java/com/bt/base/ui/BaseFragment.kt +++ b/base/src/main/java/com/bt/base/ui/BaseFragment.kt @@ -6,7 +6,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.CallSuper import androidx.annotation.LayoutRes +import androidx.annotation.MainThread import androidx.appcompat.app.AlertDialog import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding @@ -26,24 +28,37 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder abstract class BaseFragment : Fragment() { - lateinit var viewBinding: ViewBinding + private var _viewBinding: ViewBinding? = null + + // FIXME: restrict to protected modifier and correct throw expression (create new class?) + val viewBinding: ViewBinding + get() = _viewBinding ?: throw IllegalStateException( + "BaseFragment#viewBinding is only valid between BaseFragment#onCreateView(..) " + + "and BaseFragment#onDestroyView(..)" + ) abstract val viewModel: ViewModel @get:LayoutRes abstract val layoutRes: Int + // FIXME: this can be private var loadingDialog: AlertDialog? = null + // FIXME: add final modifier + @CallSuper override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - viewBinding = DataBindingUtil.inflate(inflater, layoutRes, container, false) - return viewBinding.root + ): View { + return DataBindingUtil.inflate(inflater, layoutRes, container, false).run { + _viewBinding = this + root + } } + @CallSuper override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -54,10 +69,16 @@ abstract class BaseFragment + val view = viewBinding.root + when (val e = cleanException.mapToExceptionItem(requireContext())) { is AlertExceptionItem -> { showAlertException(e) } @@ -90,6 +111,15 @@ abstract class BaseFragment(callback .build() ) { - var data = listOf() + // not thread-safe + private var cachedListRef: List? = null override fun submitList(list: MutableList?) { - data = list ?: listOf() - super.submitList(ArrayList(data)) + this.submitList(list, null) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { - return BaseViewHolder( - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - getLayoutRes(viewType), - parent, - false - ).apply { - bindFirstTime(this) - } - ) + override fun submitList(list: MutableList?, commitCallback: Runnable?) { + // sửa lỗi khi dùng adapter kết hợp với Flow hoặc LiveData emit phiên giá trị mới nhưng cùng + // tham chiếu + if (list === cachedListRef) { + notifyDataSetChanged() + } else { + super.submitList(list, commitCallback) + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): NewBaseViewHolder { + return NewBaseViewHolder(parent, getLayoutRes(viewType)) + // TODO: Delete after migration + .apply { bindFirstTime(binding) } } override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { val item = getItem(position) - holder.binding.apply { - setVariable(BR.item, item) - bindView(this, item, position) - executePendingBindings() + // TODO: use NewBaseViewHolder#bind(Item) after migration + // holder.bind(item) + @Suppress("UNCHECKED_CAST") + (holder as? NewBaseViewHolder)?.bind(item) { + bindView( + it, + item, + position + ) } } + // TODO: Delete after migration + @Deprecated("") @LayoutRes abstract fun getLayoutRes(viewType: Int): Int - protected open fun bindFirstTime(binding: ViewBinding) {} + // TODO: Delete after migration + @Deprecated( + "Adapter is responsible for ViewHolder's construction", + ReplaceWith("BaseViewHolder(ViewGroup)") + ) + protected open fun bindFirstTime(binding: ViewBinding) { + } - protected open fun bindView(binding: ViewBinding, item: Item, position: Int) {} + // TODO: Delete after migration + @Deprecated( + "Adapter is not responsible for binding new item logic of ViewHolder", + ReplaceWith("BaseViewHolder#bind(Item)") + ) + protected open fun bindView(binding: ViewBinding, item: Item, position: Int) { + } - open class BaseViewHolder constructor(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) + // TODO: Migrate to NewBaseViewModel + // this class now acts as a transition class when migrating, because there + // maybe subclasses of this class in other commits which may cause code conflicts + @Deprecated("") + open class BaseViewHolder + @Deprecated("ViewHolder decide how to create itself, will be replaced by another constructor") + constructor( + @get:Deprecated("This property should be protected") + val binding: ViewBinding, + ) : RecyclerView.ViewHolder(binding.root) } diff --git a/base/src/main/java/com/bt/base/ui/BaseRecyclerApdater2.kt b/base/src/main/java/com/bt/base/ui/BaseRecyclerApdater2.kt index bea5a3e..c9ad386 100644 --- a/base/src/main/java/com/bt/base/ui/BaseRecyclerApdater2.kt +++ b/base/src/main/java/com/bt/base/ui/BaseRecyclerApdater2.kt @@ -11,11 +11,16 @@ import com.bt.base.BR /** * Not apply DiffUtil */ -abstract class BaseRecyclerAdapter2 : RecyclerView.Adapter>() { +// FIXME: I suggest rename to: StaticBaseRecyclerAdapter | ImmutableBaseRecyclerAdapter +abstract class BaseRecyclerAdapter2 + : RecyclerView.Adapter>() { - private var data = listOf() + private val data = mutableListOf() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder2 { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BaseViewHolder2 { return BaseViewHolder2( DataBindingUtil.inflate( LayoutInflater.from(parent.context), @@ -39,17 +44,25 @@ abstract class BaseRecyclerAdapter2 : Recyc override fun getItemCount(): Int = data.size + @Deprecated("see BaseRecyclerAdapter#getLayoutRes(Int)") @LayoutRes abstract fun getLayoutRes(viewType: Int): Int - protected open fun bindFirstTime(binding: ViewBinding) {} + @Deprecated("see BaseRecyclerAdapter#bindFirstTime(ViewBinding)") + protected open fun bindFirstTime(binding: ViewBinding) { + } - protected open fun bindView(binding: ViewBinding, item: Item, position: Int) {} + @Deprecated("see BaseRecyclerAdapter#bindView(...)") + protected open fun bindView(binding: ViewBinding, item: Item, position: Int) { + } + @Deprecated("Should use constructor instead") fun setData(d: List) { - data = d + data.clear() + data.addAll(d) notifyDataSetChanged() } + // FIXME: migrate to NewBaseViewModel open class BaseViewHolder2 constructor(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) } diff --git a/base/src/main/java/com/bt/base/ui/NewBaseViewHolder.kt b/base/src/main/java/com/bt/base/ui/NewBaseViewHolder.kt new file mode 100644 index 0000000..4224b5e --- /dev/null +++ b/base/src/main/java/com/bt/base/ui/NewBaseViewHolder.kt @@ -0,0 +1,39 @@ +package com.bt.base.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes +import androidx.annotation.MainThread +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import com.bt.base.BR + +// FIXME: should be abstract? +open class NewBaseViewHolder( + parent: ViewGroup, + @LayoutRes + private val layoutId: Int, + attachToParent: Boolean = false, +) : BaseRecyclerAdapter.BaseViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + layoutId, + parent, + attachToParent, + ) +) { + @CallSuper + @MainThread + open fun bind(item: Item) { + binding.setVariable(BR.item, item) + binding.executePendingBindings() + } + + // TODO: Delete after migrating + @Deprecated("This function will be deleted after transition", ReplaceWith("#bind(Item)")) + fun bind(item: Item, apply: (Binding) -> Unit) { + apply(binding) + bind(item) + } +}