Connect viewmodel class to adapter(recyclerview) - android

I have viewModel class which name UserListViewModel And on that class there is function which named sumUserIncrease and I want to get that functions value and send that to adapter to show in recyclerview I hope you got what I mean if not, take look at this:
here is my userListViewModel:
class UserListViewModel(
val mUserInfoDAO: UserDAO,
val mTransactionDAO: TransactionsDAO,
val mLoanDAO: LoanDAO,
val mBankDAO: BankDAO,
application: Application
) :
AndroidViewModel(application) {
var viewModelJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun sumAllIncrease(id: Long): Long {
return mTransactionDAO.sumUserIncrease(id)
}
fun sumAllDecrease(id: Long): Long {
return mTransactionDAO.sumUserDecrease(id)
}
}
my Adapter:
package com.example.holyquran.ui.userList
class UserAdapter() : ListAdapter<UserInfo, RecyclerView.ViewHolder>(BillDiffCallback()) {
private val ITEM_VIEW_TYPE_EMPTY = 0
private val ITEM_VIEW_TYPE_ITEM = 1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
ITEM_VIEW_TYPE_EMPTY -> EmptyViewHolder.from(parent)
else -> throw ClassCastException("Unknown viewType $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val item = getItem(position)
holder.bind(item, clickListener)
}
is EmptyViewHolder -> {
holder.bind()
}
}
}
lateinit var clickListener: AdapterListener
fun setOnclickListener(listener: AdapterListener) {
clickListener = listener
}
override fun getItemViewType(position: Int): Int {
return if (itemCount > 0)
ITEM_VIEW_TYPE_ITEM
else
ITEM_VIEW_TYPE_EMPTY
}
class ViewHolder private constructor(val binding: ItemUserListBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: UserInfo, adapterListener: AdapterListener) {
if (item.gender == "مرد") {
binding.img.setImageResource(R.drawable.user_avata_male);
}else{
binding.img.setImageResource(R.drawable.user_avatar_female);
}
binding.userInfo = item
binding.clickListener = adapterListener
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemUserListBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
class EmptyViewHolder private constructor(val binding: ItemUserListBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind() {
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): EmptyViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemUserListBinding.inflate(layoutInflater, parent, false)
return EmptyViewHolder(binding)
}
}
}
}
class BillDiffCallback : DiffUtil.ItemCallback<UserInfo>() {
override fun areItemsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean {
return oldItem.userId == newItem.userId
}
override fun areContentsTheSame(
oldItem: UserInfo,
newItem: UserInfo
): Boolean {
return oldItem == newItem
}
}
class AdapterListener(
val clickListener: (id: Long) -> Unit,
val deleteListener: (userInfo: UserInfo) -> Unit,
private val longClickListener: (id: Long) -> Unit
) {
fun onclick(userInfo: UserInfo) = clickListener(userInfo.userId)
fun onDeleteClick(userInfo: UserInfo) = deleteListener(userInfo)
fun onLongClick(userInfo: UserInfo) = longClickListener(userInfo.userId)
}

Have your UserListViewModel contain LiveData that your Fragment/Activity observes. Once it gets an update it will send it to the Adapter.
In ViewModel
private val currentSum : MutableLiveData<Int> = MutableLiveData(0)
fun sumAllIncrease(id: Long): Long {
var sum = mTransactionDAO.sumUserIncrease(id)
currentSum.value = sum
return sum
}
fun sumAllDecrease(id: Long): Long {
var sum = mTransactionDAO.sumUserDecrease(id)
currentSum.value = sum
return sum
}
fun getCurrentSum(): LiveData<Long> {
return currentSum
}
In Fragment/Activity
viewModel.getCurrentSum().observe(this, Observer {
adapter.setSum(it)
})
In Adapter
fun setSum(sum : Long){
//The sum is now in you adapter.
//Use it how you need too.
}
But this is kinda unusual. What you really want to do is pass a new UserInfo with the changed sum. And let the DiffCallback change the one that is different. What you probably want to do is implement Paging https://proandroiddev.com/paging-3-easier-way-to-pagination-part-1-584cad1f4f61 This will allow you to change stuff in the database anywhere in the app. When that happens the data will be updated in the Adapter. Its kinda complex but once you get it working you want regret it.

Related

How To send request with different body to an Api with paging3 kotlin

Hi I have a function inside my viewModel which make a pager and return a flow:
fun loadLastMoviesList(genreId: Int) = Pager(config = PagingConfig(10)) {
LastMoviesPaging(repository, genreId)
}.flow.cachedIn(viewModelScope)
and a click listener in my HomeFragment which whenever user click it execute the function with different genreId:
genresAdapter.setOnItemClickListener { genre, name ->
lastMoviesTitle.text = "$name Movies"
genre.id?.let { id ->
lifecycleScope.launchWhenCreated {
viewModel.loadLastMoviesList(id).collect {
lastMoviesAdapter.submitData(it)
}
}
}
}
my problem is that when I click on an Item of recyclerView and it send new request with new body
the new itm's appear on the top of last item in recyclerView I want to update the new list with the last list in the recycler view when the genreId change how can I do it??
Adapter:
class LastMoviesAdapter #Inject constructor(): PagingDataAdapter<Data, LastMoviesAdapter.ViewHolder>(differCallBack) {
private lateinit var binding: ItemHomeMoviesLastBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LastMoviesAdapter.ViewHolder {
binding = ItemHomeMoviesLastBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder()
}
override fun onBindViewHolder(holder: LastMoviesAdapter.ViewHolder, position: Int) {
getItem(position)?.let { holder.setData(it) }
holder.setIsRecyclable(false)
}
inner class ViewHolder: RecyclerView.ViewHolder(binding.root){
fun setData(item: Data){
binding.apply {
movieNameTxt.text = item.title
movieRateTxt.text = item.imdbRating
movieCountryTxt.text = item.country
movieYearTxt.text = item.year
moviePosterImg.load(item.poster){
crossfade(true)
crossfade(800)
}
root.setOnClickListener{
onItemClickListener?.let {
it(item)
}
}
}
}
}
private var onItemClickListener: ((Data) -> Unit)? = null
fun setOnItemClickListener(listener: (Data) -> Unit){
onItemClickListener = listener
}
companion object{
val differCallBack = object : DiffUtil.ItemCallback<Data>(){
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem == newItem
}
}
}
pagingSourceClase:
class LastMoviesPaging (private val repository: HomeRepository, private val genreId: Int): PagingSource<Int, Data>() {
override fun getRefreshKey(state: PagingState<Int, Data>): Int? {
return null
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
return try {
val currentPage = params.key ?: 1
Log.d("paging3", "paging running genre: $genreId")
val responseData = repository.lastMoviesList(genreId, currentPage).run {
body()?.data?.toString()?.let { Log.d("paging3", it) }
body()?.data ?: emptyList()
}
LoadResult.Page(responseData, if (currentPage == 1) null else -1 , if (responseData.isEmpty()) null else currentPage + 1)
}catch (e: Exception){
LoadResult.Error(e)
}
}
and I also want to save the flow in viewModel instead of resiving it directly in the HomeFragment
so any one can suggest a solution for my problem? I will provide more information if needed

Having 2 model class(dataClass) in adapter(ListAdapter)

Recently, I was faced with an issue that is a bit hard for me to solve
I need to put 2 models in the List adapter, but it always says 2 types of arguments expected
Here is the Link of the whole project
and I want to have Transaction and Bank models in ListAdapter
you can read the project README.
The adapter that I want to have 2 models is in ui.TransactionHistory
here is my whole adapter class:
class TransactionHistory() :
ListAdapter<Transaction, RecyclerView.ViewHolder>(BillDiffCallback()) {
private val ITEM_VIEW_TYPE_EMPTY = 0
private val ITEM_VIEW_TYPE_ITEM = 1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
ITEM_VIEW_TYPE_EMPTY -> EmptyViewHolder.from(parent)
else -> throw ClassCastException("Unknown viewType $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val item = getItem(position)
holder.bind(item, clickListener)
// holder.bind2(Bank, clickListener)
}
is EmptyViewHolder -> {
holder.bind()
}
}
}
lateinit var clickListener: AdapterListener2
fun setOnclickListener(listener: AdapterListener2) {
clickListener = listener
}
override fun getItemViewType(position: Int): Int {
return if (itemCount > 0)
ITEM_VIEW_TYPE_ITEM
else
ITEM_VIEW_TYPE_EMPTY
}
class ViewHolder
private constructor(val binding: ItemUserTransactionListBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Transaction, adapterListener2: AdapterListener2) {
binding.transaction = item
binding.clickListener = adapterListener2
binding.executePendingBindings()
if (item.type == "payPayment") {
binding.transactionStatus.text = "برداخت قسط"
} else if (item.type == "decrease") {
binding.transactionStatus.text = "برداشت"
} else if (item.type == "increase") {
binding.transactionStatus.text = "واریز"
}
if (item.decrease == null) {
binding.amount.text = item.increase
} else {
binding.amount.text = item.decrease
}
}
fun bind2(item2: Bank, adapterListener2: AdapterListener2) {
binding.bankInfo = item2
binding.clickListener = adapterListener2
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemUserTransactionListBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class EmptyViewHolder private constructor(val binding: ItemUserTransactionListBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind() {
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): EmptyViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemUserTransactionListBinding.inflate(layoutInflater, parent, false)
return EmptyViewHolder(binding)
}
}
}
class BillDiffCallback : DiffUtil.ItemCallback<Transaction>() {
override fun areItemsTheSame(oldItem: Transaction, newItem: Transaction): Boolean {
return oldItem.transId == newItem.transId
}
override fun areContentsTheSame(
oldItem: Transaction,
newItem: Transaction
): Boolean {
return oldItem == newItem
}
}
class AdapterListener2(
val clickListener: (id: Long?) -> Unit,
val deleteListener: (category: Transaction) -> Unit
) {
fun onclick(transaction: Transaction) = clickListener(transaction.userId)
fun onDeleteClick(userInfo: Transaction) = deleteListener(userInfo)
}
whenever i placed secound model here:
ListAdapter<Transaction, **Bank**, RecyclerView.ViewHolder>(BillDiffCallback()) {}
it says 2 type argument expected.
I don't know if it helps you or not but some one told me I have to use join in Kotlin
THANKS FOR YOUR ANSWERS :)
ListAdaper can only accept one data model however you can add multiple items using another class like Sealed Class
sealed class DataItem {
abstract val id: Long
data class TransactionItem(val transaction: Transaction): DataItem() {
override val id = transaction.transId
}
object Empty: DataItem() {
override val id = Long.MIN_VALUE
}
}
and deal with this only class in your ListAdapter for your code you will need to apply this changes
class TransactionHistory() :
ListAdapter<DataItem, RecyclerView.ViewHolder>(BillDiffCallback()) {
private val ITEM_VIEW_TYPE_EMPTY = 0
private val ITEM_VIEW_TYPE_ITEM_TRANSACTION = 1
private val ITEM_VIEW_TYPE_ITEM_BANK = 2
private val adapterScope = CoroutineScope(Dispatchers.Default)
/**
* DO NOT USE .submit(), use the method bellow
*/
fun addTransactionsAndBanks(transactionList: List<Transaction>?, bankList: List<Bank>?) {
adapterScope.launch {
val transactionItems: List<DataItem> = when {
transactionList == null || transactionList.isEmpty() -> listOf(DataItem.Empty)
else -> transactionList.map { DataItem.TransactionItem(it) }
}
val bankItems: List<DataItem> = when {
bankList == null || bankList.isEmpty() -> listOf(DataItem.Empty)
else -> bankList.map { DataItem.BankItem(it) }
}
val items = transactionItems + bankItems
withContext(Dispatchers.Main) {
submitList(items)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_ITEM_TRANSACTION -> ViewHolder.from(parent)
ITEM_VIEW_TYPE_ITEM_BANK -> ViewHolder.from(parent)
ITEM_VIEW_TYPE_EMPTY -> EmptyViewHolder.from(parent)
else -> throw ClassCastException("Unknown viewType $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
when (val item = getItem(position)) {
is DataItem.TransactionItem -> holder.bind(item.transaction, clickListener)
is DataItem.BankItem -> holder.bind2(item.bank, clickListener)
}
}
is EmptyViewHolder -> holder.bind()
}
}
lateinit var clickListener: AdapterListener2
fun setOnclickListener(listener: AdapterListener2) {
clickListener = listener
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.Empty -> ITEM_VIEW_TYPE_EMPTY
is DataItem.TransactionItem -> ITEM_VIEW_TYPE_ITEM_TRANSACTION
is DataItem.BankItem -> ITEM_VIEW_TYPE_ITEM_BANK
}
}
class ViewHolder
private constructor(val binding: ItemUserTransactionListBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Transaction, adapterListener2: AdapterListener2) {
binding.transaction = item
binding.clickListener = adapterListener2
binding.executePendingBindings()
if (item.type == "payPayment") {
binding.transactionStatus.text = "برداخت قسط"
} else if (item.type == "decrease") {
binding.transactionStatus.text = "برداشت"
} else if (item.type == "increase") {
binding.transactionStatus.text = "واریز"
}
if (item.decrease == null) {
binding.amount.text = item.increase
} else {
binding.amount.text = item.decrease
}
}
fun bind2(item2: Bank, adapterListener2: AdapterListener2) {
binding.bankInfo = item2
binding.clickListener = adapterListener2
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemUserTransactionListBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class EmptyViewHolder private constructor(val binding: ItemUserTransactionListBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind() {
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): EmptyViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemUserTransactionListBinding.inflate(layoutInflater, parent, false)
return EmptyViewHolder(binding)
}
}
}
class BillDiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
class AdapterListener2(
val clickListener: (id: Long?) -> Unit,
val deleteListener: (category: Transaction) -> Unit
) {
fun onclick(transaction: Transaction) = clickListener(transaction.userId)
fun onDeleteClick(userInfo: Transaction) = deleteListener(userInfo)
}
sealed class DataItem {
abstract val id: Long
data class TransactionItem(val transaction: Transaction) : DataItem() {
override val id = transaction.transId
}
data class BankItem(val bank: Bank) : DataItem() {
override val id = bank.bankId
}
object Empty : DataItem() {
override val id = Long.MIN_VALUE
}
}
You can build the list based on only a single data source. If you want to have multiple data sources, you should create a third data model and add the other two models in it.
data class ListData(val transaction : Transaction , val Bank : Bank)
ListView/Recycler view will create number of list items based on the list of objects passed.
ListAdapter<ListData, RecyclerView.ViewHolder>
I found the answer to my Question
We have to use Join in query:
//for single info
#Query("SELECT `transaction`.increase, `transaction`.decrease, bank.bank_name From `transaction` JOIN bank WHERE `transaction`.bank_id=:key ")
fun joinTables(key: Long): LiveData<TransactionAndBank>?
//for list of info
#Query("SELECT `transaction`.increase, `transaction`.decrease,`transaction`.type, bank.bank_name From `transaction` JOIN bank WHERE `transaction`.bank_id=:key ")
fun joinAllTables(key: Long): LiveData<List<TransactionAndBank>>
I had to place it In TransactionDAO
source

The correct way to use multiple Viewholders in Listadapter

I was following this tutorial and tried to use multiple ViewHolders. An example use case would be a messaging app where the recyclerview has to display a sent message and a receiving message with different layouts. I got it working with the following solution:
class MyEntityDiffCallback : DiffUtil.ItemCallback<MyEntity>() {
override fun areItemsTheSame(oldItem: MyEntity, newItem: MyEntity): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MyEntity, newItem: MyEntity): Boolean {
return oldItem == newItem
}
}
class MyAdapter :
ListAdapter<MyEntity, MyAdapter.MyAbstractViewHolder>(
MyEntityDiffCallback()
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MyAbstractViewHolder {
if (viewType == 1){
return MyViewHolder1.from(parent)
}else{
return MyViewHolder2.from(parent)
}
}
override fun onBindViewHolder(holder: MyAbstractViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
if(item.someCounter > 5){
return 1
}else{
return 0
}
}
abstract class MyAbstractViewHolder(val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: MyEntity)
}
class MyViewHolder2 private constructor(binding: Item2Binding) :
MyAbstractViewHolder(binding) {
override fun bind(
item: MyEntity
) {
val binding = binding as Item2Binding
binding.message = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): MyViewHolder2 {
val layoutInflater = LayoutInflater.from(parent.context)
val binding =
Item2Binding.inflate(layoutInflater, parent, false)
return MyViewHolder2(binding)
}
}
}
class MyViewHolder1 private constructor(binding: Item1Binding) :
MyAbstractViewHolder(binding) {
override fun bind(
item: MyEntity
) {
val binding = binding as Item1Binding
binding.message = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): MyViewHolder1 {
val layoutInflater = LayoutInflater.from(parent.context)
val binding =
Item1Binding.inflate(layoutInflater, parent, false)
return MyViewHolder1(binding)
}
}
}
}
Is this the way it should be implemented? I am not sure about the cast of the binding value in the bind method. Or are there other good practices to solve this with the Listadapter and the Databinding?

TabLayout jumps to start of its witdth after notifyItemChanged()

I'm building quiz app and all the time I have trouble with notifying RecyclerView about updated item.
DiffUtil didn't work for me, anyway - I can use adapter.notifyItemChanged() but after this call my TabLayout jumps to start of it's width.
I have set onClickListener to my floatingButton to be sure that it's notifyItemChanged() issue.
Am I doing something wrong?
Fragment
class TestFragment : Fragment() {
private lateinit var viewModel: TestViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentTestBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_test,
container,
false
)
val args = TestFragmentArgs.fromBundle(requireArguments())
val viewModelFactory = TestViewModelFactory(args.course)
viewModel = ViewModelProvider(this, viewModelFactory).get(TestViewModel::class.java)
binding.viewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
val adapter = PagerAdapter(ClickListener { questionDatabase: QuestionDatabase, string: String ->
val questionUpdated = questionDatabase.apply { answer = string }
viewModel.updateAnswer(questionDatabase, questionUpdated)
})
binding.viewPager.adapter = adapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab: TabLayout.Tab, i: Int ->
tab.text = (i + 1).toString()
}.attach()
viewModel.pytania.observe(viewLifecycleOwner, Observer {
adapter.differ.submitList(it)
})
viewModel.score.observe(viewLifecycleOwner, Observer {
if (it != null) {
findNavController().navigate(
TestFragmentDirections.actionTestFragmentToScoreFragment(it)
)
viewModel.score.value = null
}
})
binding.bttnFinish.setOnClickListener {
adapter.notifyItemChanged(binding.viewPager.currentItem)
}
return binding.root
}
}
ViewModel
class TestViewModel(
test: String
): ViewModel() {
private val _pytania = MutableLiveData<List<QuestionDatabase>>()
val pytania: LiveData<List<QuestionDatabase>>
get() = _pytania
val score = MutableLiveData<Int?>()
init {
val listOfQuestions = mutableListOf<QuestionDatabase>()
for (x in 1..15) {
listOfQuestions.add(
QuestionDatabase(
question = "Question $x",
answerA = "AnswerA",
answerB = "AnswerB",
answerC = "AnswerC",
answerD = "AnswerD",
correctAnswer = "AnswerD",
image = 0,
questionNumber = 0
)
)
}
_pytania.value = listOfQuestions.toList()
}
fun updateAnswer(questionDatabase: QuestionDatabase, questionUpdated: QuestionDatabase) {
_pytania.value = _pytania.value?.replace(questionDatabase, questionUpdated)
}
}
ViewPagerAdapter
class PagerAdapter(
private val clickListener: ClickListener
) : RecyclerView.Adapter<ViewHolder>() {
private val differCallBack = object : DiffUtil.ItemCallback<QuestionDatabase>() {
override fun areItemsTheSame(oldItem: QuestionDatabase, newItem: QuestionDatabase): Boolean {
return oldItem.question == newItem.question
}
override fun areContentsTheSame(oldItem: QuestionDatabase, newItem: QuestionDatabase): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallBack)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(differ.currentList[position], clickListener)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
class ViewHolder private constructor(val binding: VpItemQuestionBinding): RecyclerView.ViewHolder(
binding.root
) {
fun bind(
question: QuestionDatabase,
clickListener: ClickListener
) {
binding.question = question
binding.onClickListener = clickListener
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = VpItemQuestionBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
class ClickListener(val clickListener: (QuestionDatabase, string: String) -> Unit) {
fun onClick(question: QuestionDatabase, string: String) = clickListener(question, string)
}
Before notifyItemChanged()
After notifyItemChanged()

Handling button clicks in recyclerview adapter (Kotlin)?

I have an adapter in which the items each have 3 buttons, that generate a dialog that then performs an action. I have a sense that this should be removed from the adapter (I have view models available), but it works and I am wondering: Should I move logic to the fragment, to the view model, do I need to move it at all (is the code below bad practice and if so why)? Any help/input would be greatly appreciated.
Here is the adapter code:
class ViewRecipesAdapter(val context: Context, private val recipes: List<Recipe>, private val parentFragment: Fragment) :
RecyclerView.Adapter<ViewRecipesAdapter.RecipeViewHolder>()
{
private var listToUse: List<Recipe> = recipes
private lateinit var recipesViewModel: RecipesViewModel
private var isView = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder
{
val layoutInflater = LayoutInflater.from(parent.context)
val binding: ViewRecipesItemBinding =
DataBindingUtil.inflate(layoutInflater, R.layout.view_recipes_item, parent, false)
return RecipeViewHolder(binding, context)
}
override fun getItemCount() = listToUse.size
override fun onBindViewHolder(holder: RecipeViewHolder, position: Int)
{
val recipe = listToUse[position]
// to delete and edit items
val dao = RecipesDatabase.getInstance(context).recipeDao()
val repository = RecipeRepository(dao)
recipesViewModel = RecipesViewModel(repository)
//display data on list item
holder.bind(recipe)
Glide.with(context).load(recipe.imageOne)
.into(holder.binding.imageViewItemImage)
//tried to handle clicks here through the viewModel but I could not get it working from fragment
//the function call after viewModel calls is what works and it seems to work well
holder.binding.imageButtonItemdelete.setOnClickListener {
recipesViewModel.setIsDelete(true)
recipesViewModel.setPositionFromAdapter(position)
startDeleteDialog(position)
}
holder.binding.imageButtonItemedit.setOnClickListener {
recipesViewModel.setIsView(false)
recipesViewModel.setPositionFromAdapter(position)
isView = false
startEditOrViewDialog(position)
}
holder.binding.imageButtonItemview.setOnClickListener {
recipesViewModel.setIsView(true)
recipesViewModel.setPositionFromAdapter(position)
isView = true
startEditOrViewDialog(position)
}
}
fun setList(newList: List<Recipe>)
{
listToUse = newList
}
//dialog functions for the edit, delete, and view buttons on each item
private fun startDeleteDialog(position: Int)
{
AlertDialog.Builder(context)
.setTitle("Delete recipe?")
.setPositiveButton("Yes") { _, _ ->
recipesViewModel.deleteRecipe(recipes[position])
notifyItemRemoved(position)
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}.show()
}
private fun startEditOrViewDialog(position: Int)
{
when (isView)
{
true ->
{
AlertDialog.Builder(context).setTitle("View recipe?")
.setPositiveButton("Yes") { _, _ ->
//get relevant data from current recipe
val recipe = recipes[position]
//create a dialog that shows this data in an inflated layout
val viewDialog = AlertDialog.Builder(context)
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.fragment_edit_or_view, null)
view.editText_editrecipe_directions.setText(recipe.directions)
view.editText_editrecipe_ingredients.setText(recipe.ingredients)
view.editText_editrecipe_notes.setText(recipe.notes)
view.editText_editrecipe_title.setText(recipe.title)
view.textView_date_edit.text = recipe.date
view.editText_editrecipe_title.keyListener = null
view.editText_editrecipe_directions.keyListener = null
view.editText_editrecipe_ingredients.keyListener = null
view.editText_editrecipe_notes.keyListener = null
if (recipe.rating != null)
{
view.ratingBar_edit.rating = recipe.rating
}
Glide.with(context)
.load(recipe.imageOne)
.into(view.imageView_addphoto_edit)
viewDialog.setView(view).show()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}.show()
}
false ->
{
AlertDialog.Builder(context).setTitle("Edit recipe?")
.setPositiveButton("Yes") { _, _ ->
//get relevant data from current recipe
val recipe = recipes[position]
val idString = recipe.id.toString()
recipesViewModel.setId(idString)
recipesViewModel.getRecipeById2(idString)
notifyDataSetChanged()
val controller = parentFragment.findNavController()
controller.navigate(
ViewRecipesFragmentDirections.actionNavViewrecipesToNavAddrecipe(
recipe.id.toString()
)
)
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}.show()
}
}
}
override fun getItemId(position: Int): Long
{
return position.toLong()
}
override fun getItemViewType(position: Int): Int
{
return position
}
class RecipeViewHolder(val binding: ViewRecipesItemBinding, val context: Context) :
RecyclerView.ViewHolder(binding.root)
{
fun bind(recipe: Recipe)
{
if (recipe.isLeftover == true)
{
binding.tvIsLeftovers.visibility = View.VISIBLE
}
binding.textViewItemTitle.text = recipe.title
if (recipe.date != null)
{
binding.textViewItemDate.text = recipe.date
}
if (recipe.rating != null)
{
binding.ratingBar2.rating = recipe.rating
}
binding.root.animation = AlphaAnimation(0.0f, 1.0f).apply {
duration = 1000
}
}
}
}
This is the view model, with live data variables set up that I could not get working in the fragment that this RecyclerView is in:
class RecipesViewModel(private val repository: RecipeRepository) : ViewModel()
{
val recipesList = repository.getAllRecipes()
private val _isView = MutableLiveData<Boolean>()
val isView: MutableLiveData<Boolean> = _isView
private val _isEdit = MutableLiveData<Boolean>()
val isEdit: MutableLiveData<Boolean> = _isEdit
private val _positionFromAdapter = MutableLiveData<Int>()
val positionFromAdapter: MutableLiveData<Int> = _positionFromAdapter
private val _isDelete = MutableLiveData<Boolean>()
val isDelete: MutableLiveData<Boolean> = _isDelete
private val _recipesListFromSearch = MutableLiveData<List<Recipe>>()
val recipesListFromSearch: LiveData<List<Recipe>> = _recipesListFromSearch
private val _recipe = MutableLiveData<Recipe>()
val recipe: LiveData<Recipe> = _recipe
lateinit var searchString: String
val savedId = MutableLiveData<String>()
fun setPositionFromAdapter(position: Int)
{
_positionFromAdapter.value = position
}
fun setIsView(isView: Boolean)
{
_isView.value = isView
}
fun setIsDelete(isDelete: Boolean)
{
_isView.value = isDelete
}
fun setIsEdit(isEdit: Boolean)
{
_isEdit.value = isEdit
}
fun setId(id: String)
{
savedId.value = id
}
fun insertRecipe(recipe: Recipe)
{
CoroutineScope(Dispatchers.IO).launch {
repository.insertRecipe(recipe)
}
}
fun getRecipesFromQuery(query: String)
{
CoroutineScope(Dispatchers.IO).launch {
val list = repository.getRecipesSearch(query)
MainScope().launch { _recipesListFromSearch.value = list }
}
}
fun saveUserRecipeToDb(
title: String?,
ingredients: String?,
directions: String?,
notes: String?,
uriToSave: String?,
rating: Float?,
date: String?,
isLeftover: Boolean,
loadedId: String
): Boolean
{
val recipeToSave = Recipe(
title,
ingredients,
directions,
notes,
uriToSave,
null,
null,
rating,
date,
isLeftover
)
if (loadedId != "666")
{
recipeToSave.id = loadedId.toInt()
}
insertRecipe(recipeToSave)
return false
}
fun getRecipeById2(id: String) = repository.getRecipeByIdLive(id)
fun deleteRecipe(recipe: Recipe)
{
CoroutineScope(Dispatchers.IO).launch {
repository.deleteRecipe(recipe)
}
}
}
How to implement onClick in the RecyclerView. Let's assume that in Your Recycler every view is a visualization of some item and when You click on it You want to do something with that item:
Create class: ClickListener:
class ClickListener(
val clickListener: (itemId: Int) -> Unit,
)
{
fun onClick(item: ItemClass) = clickListener(item.id)
}
Now in Your RecylerViewAdapter pass as an argument this Listener:
class RecylerViewAdapter(
private val clickListener: ClickListener
)
In onBindViewHolder pass this Listenner as argument
override fun onBindViewHolder(holder: ViewHolder, position: Int)
{
holder.bind(getItem(position)!!, clickListener)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
{
return ViewHolder.from(
parent
)
}
In Your ViewHolder class:
class ViewHolder private constructor(private val binding: ItemRecyclerBinding) :
RecyclerView.ViewHolder(binding.root)
{
companion object
{
fun from(parent: ViewGroup): ViewHolder
{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemRecyclerBinding.inflate(layoutInflater, parent, false)
return ViewHolder(
binding
)
}
}
fun bind(
item : Item,
clickListener: ClickListener
)
{
binding.item = item
binding.clickListener = clickListener
binding.executePendingBindings()
}
}
In Your item layout (which has to be converted to data binding layout) add this:
<data>
<variable
name="item"
type="com.example.sth.database.Item" /> // path to `Item`
<variable
name="clickListener"
type="com.example.sth.ui.adapter.ClickListener" /> // Path to `ClickListener`
</data>
Now You can add onClick method to Button:
android:onClick="#{() -> clickListener.onClick(item)}"
When You create Adapter in fragment or Activity You have to pass clickListenner as a parameter. In this way You can handle everything from fragment and RecyclerView doesn't care about what You do in this function.
val clickListenner = ClickListenner(
{ id -> viewModel.clickItemWithid(id) }, // click. This function from ViewModel will be executed when You click on item in recycler View
)
val adapter = RecylerViewAdapter (
clickListenner
)
This method is based on Google developers codelabs on Udacity.
Here You can check whole codelabs. It is free.
And here is just one video with implementing click listenner
These are the changes that are working for me now:
class ClickListener(val clickListener: (itemId: Int, itemPosition: Int, dialogInt: Int) -> Unit) {
fun onClickDelete(recipe: Recipe, position: Int, dialogInt: Int) = clickListener(recipe.id, position, dialogInt)
fun onClickEdit(recipe: Recipe, position: Int, dialogInt: Int) = clickListener(recipe.id, position, dialogInt)
fun onClickView(recipe: Recipe, position: Int, dialogInt: Int) = clickListener(recipe.id, position, dialogInt)
}
In the adapter:
class RecipeViewHolder private constructor(val binding: ViewRecipesItemBinding) :
RecyclerView.ViewHolder(binding.root) {
companion object {
fun from(parent: ViewGroup): RecipeViewHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ViewRecipesItemBinding.inflate(layoutInflater, parent, false)
return RecipeViewHolder(binding)
}
}
fun bind(recipe: Recipe, clickListener: ClickListener) {
binding.recipe = recipe
binding.imageButtonItemdelete.setOnClickListener {
clickListener.onClickDelete(recipe, adapterPosition, 1)
}
binding.imageButtonItemedit.setOnClickListener {
clickListener.onClickEdit(recipe, adapterPosition,2)
}
binding.imageButtonItemview.setOnClickListener {
clickListener.onClickView(recipe, adapterPosition,3)
}
binding.executePendingBindings()
binding.root.animation = AlphaAnimation(0.0f, 1.0f).apply {
duration = 1000
}
}
In the fragment holding the RecyclerView :
private fun initRecyclerView() {
recipesViewModel.recipesList.observe(viewLifecycleOwner, Observer {
//update recyclerview
val list = it
listForFragment = it
clickListener = ClickListener { id, position, dialogInt ->
recipesViewModel.apply {
setPositionFromAdapter(position)
setDialogRecipe(id)
}
when (dialogInt) {
1 -> startDeleteDialog(position)
2 -> startEditDialog(position)
3 -> startViewDialog(position)
}
}
rv_viewrecipes.adapter = ViewRecipesAdapter(requireContext(), list, this, clickListener)
rv_viewrecipes.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
})
}
I couldn't figure out how to pass the position and int for the when statement through the xml onClick, but for now at least the adapter class has the view logic removed.

Categories

Resources