findContainingViewHolder returns null - java

In the following application, you have basically 2 fragments: 1) a food database, 2) a consumed food list
1) the user can manually add foods with the corresponding macronutrient information (kcal, carbs, protein, etc.). The data is stored in a SQL database via Room and is being observed via LiveData
2) a fragment (FoodDiaryFragment.kt) that shows the "consumed" food via a recyclerView. The user can add foods by tapping on a FAB which sends the user to another fragment (AddConsumedFoodFragment.kt).
this shows a spinner and a form. The spinner shows a list of the foods from the food database (from 1). In the form the user can just enter one value (how much gram of the selected food has been consumed).
This data is also added to the same SQL database in a table with: id (autogenerated), amount (entered by the user), consumedFood (selected from the spinner and connected to the other table via a foreign key) and consumedDate (autogenerated date at the moment when the item is added).
The recycler view (2) shows the items with additional information which are calculated in the RecyclerViewAdapter (basically multiplying the amount and the corresponding macronutrient info [kcal, carbs, etc.], see getDailyValues() in the adapter) and also "groups" the items by date. This means, for each day a separate viewHolder (DateViewHolder) which displays the day (and the sum of each macro) is being added to the recyclerView (see sortAndGroupFood() in FoodDiaryFragment.kt)
As I am using RecyclerView Selection setStableIds() (see init block) is set to true. StableIdKeyProvider.java adds an ChildAttachStateChangeListener with the onChilViewDetachedFromWindows() function.
I am not sure exactly when this function (I would love an explanation) is called but this seems to happen sometimes, when either an Item is deleted (items can be deleted via RecyclerView.Selection) or an item is added. The app crashes with the below shown NullPointerException. When the app is restarted the action that eventually ended in the crash has been conducted successfully.
I am really desperate and have tried to figure out why a null view is passed to this function for several hours now, but obviously I have not been succesful.
According to the docs findContainingViewHolder returns null if the provided view is not a descendant of that RecyclerView
Question: What problem causes the return of null and how can I precent it?
GitHub Depo Link
FoodDiaryFragment.kt
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.selection.*
import androidx.recyclerview.widget.RecyclerView
import com.hooni.macrotracker.R
import com.hooni.macrotracker.adapter.ConsumedFoodRecyclerViewAdapter
import com.hooni.macrotracker.data.ConsumedFood
import com.hooni.macrotracker.recyclerviewselector.ConsumedFoodItemDetailsLookup
import com.hooni.macrotracker.viewmodels.FoodViewModel
import kotlinx.android.synthetic.main.fragment_food_diary.view.*
import java.text.DateFormat
import java.text.SimpleDateFormat
class FoodDiaryFragment : Fragment() {
private lateinit var foodViewModel: FoodViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: ConsumedFoodRecyclerViewAdapter
private val dateFormat = DateFormat.getDateInstance()
var tracker: SelectionTracker<Long>? = null
var actionMode: ActionMode? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val v = inflater.inflate(R.layout.fragment_food_diary, container, false)
initRecyclerView(v)
initViewModel()
initButtons(v)
// this makes sure, that in case the last destination was addConsumedFoodFragment
// the 'back' button doesn't bring you back to the addConsumedFoodFragment, but to the one that
// has been visited before
findNavController().popBackStack(R.id.addConsumedFoodFragment, true)
return v
}
private fun initButtons(v: View) {
val addNewFood = v.addNewConsumedFood
addNewFood.setOnClickListener {
findNavController().navigate(R.id.action_diaryFragment_to_addConsumedFoodFragment)
}
}
private fun initRecyclerView(view: View) {
recyclerView = view.food_diary_recyclerView
adapter = ConsumedFoodRecyclerViewAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
tracker = SelectionTracker.Builder<Long>(
"selectedItemsConsumedFoo",
recyclerView,
StableIdKeyProvider(recyclerView),
ConsumedFoodItemDetailsLookup(recyclerView),
StorageStrategy.createLongStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
val actionModeCallBack = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
val inflater = mode?.menuInflater
actionMode = mode
actionMode?.title = getString(R.string.delete)
inflater?.inflate(R.menu.action_menu, menu)
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return when (item?.itemId) {
R.id.action_menu_delete -> {
removeItems(tracker?.selection!!)
tracker?.clearSelection()
actionMode?.finish()
actionMode = null
return true
}
else -> false
}
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
tracker?.clearSelection()
actionMode?.finish()
actionMode = null
}
}
tracker?.addObserver(
object : SelectionTracker.SelectionObserver<Long>() {
override fun onSelectionChanged() {
if (tracker?.selection!!.size() > 0) {
if (actionMode == null) activity?.startActionMode(actionModeCallBack)
} else {
actionMode?.finish()
actionMode = null
}
}
}
)
adapter.tracker = tracker
}
private fun removeItems(selection: Selection<Long>) {
val foodsToDelete = adapter.getFood(selection)
foodsToDelete.forEach {
foodViewModel.deleteConsumedFood(it)
}
}
private fun initViewModel() {
foodViewModel = ViewModelProvider(this).get(FoodViewModel::class.java)
foodViewModel.allConsumedFood.observe(viewLifecycleOwner, Observer { consumedFoodList ->
consumedFoodList?.let { adapter.setConsumedFood(sortAndGroupFood(consumedFoodList)) }
})
foodViewModel.allFood.observe(viewLifecycleOwner, Observer { food ->
food?.let { adapter.setFood(food.sortedBy {it.foodName}) }
})
}
private fun sortAndGroupFood(consumedFoodList: List<ConsumedFood>): List<ConsumedFood> {
val sortedList = consumedFoodList.sortedBy{it.consumedDate}
val groupedMap: Map<String,List<ConsumedFood>> = sortedList.groupBy { dateFormat.format(it.consumedDate)}
val finalizedList = mutableListOf<ConsumedFood>()
groupedMap.forEach {
finalizedList.add(createDateHeader(it.key))
finalizedList.addAll(it.value)
}
return finalizedList.toList()
}
private fun createDateHeader(dateString: String): ConsumedFood {
val date = dateFormat.parse(dateString)
return ConsumedFood(null,-1,"DATE_HEADER",date!!)
}
}
AddFoodFragment.kt
package com.hooni.macrotracker.fragments
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.google.android.material.textfield.TextInputLayout
import com.hooni.macrotracker.R
import com.hooni.macrotracker.data.Food
import com.hooni.macrotracker.util.Tools
import com.hooni.macrotracker.viewmodels.FoodViewModel
import kotlinx.android.synthetic.main.fragment_add_food.view.*
import java.text.DecimalFormatSymbols
import java.util.*
class AddFoodFragment : Fragment() {
private lateinit var foodViewModel: FoodViewModel
private lateinit var enterTextFoodName: TextInputLayout
private lateinit var enterTextKcal: TextInputLayout
private lateinit var enterTextCarbs: TextInputLayout
private lateinit var enterTextProtein: TextInputLayout
private lateinit var enterTextFat: TextInputLayout
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val v = inflater.inflate(R.layout.fragment_add_food, container, false)
initButtons(v)
initViewModel()
initTextInPutLayouts(v)
return v
}
private fun initTextInPutLayouts(v: View) {
enterTextFoodName = v.enter_food_name
enterTextKcal = v.enter_kcal
enterTextCarbs = v.enter_carbs
enterTextProtein = v.enter_protein
enterTextFat = v.enter_fat
enterTextCarbs.setNumberDecimalInputOnly()
enterTextProtein.setNumberDecimalInputOnly()
enterTextFat.setNumberDecimalInputOnly()
}
private fun initButtons(view: View) {
view.cancel_add_food.setOnClickListener {
findNavController().navigate(R.id.action_addFoodFragment_to_foodListFragment)
Tools.hideSoftKeyboard(view, context)
}
view.add_food.setOnClickListener {
if (validateValues()) {
val newFood = createNewFood()
foodViewModel.insertFood(newFood)
showSuccessfulAdd()
view.cancel_add_food.performClick()
}
}
}
private fun validateValues(): Boolean {
val isFoodNameValid = enterTextFoodName.validateInput { it != null }
val isKcalValid = enterTextKcal.validateInput { it?.toIntOrNull() != null }
val isCarbsValid = enterTextCarbs.validateInput { it?.toDoubleOrNull() != null }
val isProteinValid = enterTextProtein.validateInput { it?.toDoubleOrNull() != null }
val isFatValid = enterTextFat.validateInput { it?.toDoubleOrNull() != null }
return (isFoodNameValid
&& isKcalValid
&& isCarbsValid
&& isProteinValid
&& isFatValid)
}
private fun initViewModel() {
foodViewModel = ViewModelProvider(this).get(FoodViewModel::class.java)
}
private fun createNewFood(): Food {
val foodName =
enterTextFoodName.editText?.text.toString().trim()
val kcal =
enterTextKcal.editText?.text.toString().trim().toInt()
val carbs =
enterTextCarbs.editText?.text.toString().replaceDecimalSeparator().toDouble()
val protein =
enterTextProtein.editText?.text.toString().replaceDecimalSeparator().toDouble()
val fat = enterTextFat.editText?.text.toString().replaceDecimalSeparator().toDouble()
return Food(foodName, kcal, carbs.round(), fat.round(), protein.round())
}
private fun showSuccessfulAdd() {
Toast.makeText(
activity,
"${enterTextFoodName.editText?.text.toString().trim()} added to database",
Toast.LENGTH_SHORT
).show()
}
private inline fun TextInputLayout.validateInput(validate: (String?) -> Boolean): Boolean {
val textToValidate = this.editText?.text.toString().replaceDecimalSeparator().trim()
when {
textToValidate.isEmpty() -> {
error = getString(R.string.cant_be_empty)
}
!validate(textToValidate) -> {
error = getString(R.string.invalid_value)
}
else -> {
error = null
return true
}
}
return false
}
private fun TextInputLayout.setNumberDecimalInputOnly() {
this.editText?.apply {
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
}
}
private fun Double.round(decimals: Int = 1): Double = "%.${decimals}f".format(Locale.US,this).toDouble()
#SuppressLint("NewApi")
private fun String.replaceDecimalSeparator(): String {
val decimalSeparator = when(Build.VERSION.SDK_INT) {
in Int.MIN_VALUE..Build.VERSION_CODES.M -> {DecimalFormatSymbols(resources.configuration.locale).decimalSeparator}
else -> DecimalFormatSymbols(resources.configuration.locales[0]).decimalSeparator
}
return this.replace(decimalSeparator, '.')
}
}
ConsumedFoodRecyclerViewAdapter.kt
package com.hooni.macrotracker.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.Selection
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.RecyclerView
import com.hooni.macrotracker.R
import com.hooni.macrotracker.data.ConsumedFood
import com.hooni.macrotracker.data.Food
import kotlinx.android.synthetic.main.fragment_food_diary_date_list_item.view.*
import kotlinx.android.synthetic.main.fragment_food_diary_list_item.view.*
import java.text.DateFormat
import java.util.*
private const val DATE_VIEW_HOLDER = 0
private const val CONSUMED_FOOD_VIEW_HOLDER = 1
class ConsumedFoodRecyclerViewAdapter : RecyclerView.Adapter<ConsumedFoodRecyclerViewAdapter.BaseViewHolder<*>>() {
private var mConsumedFoodList = emptyList<ConsumedFood>()
private var mFoodList = emptyList<Food>()
var tracker: SelectionTracker<Long>? = null
init {
setHasStableIds(true)
}
abstract class BaseViewHolder<T>(itemView: View): RecyclerView.ViewHolder(itemView) {
abstract fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long>
}
inner class ConsumedFoodViewHolder(mView: View) :BaseViewHolder<ConsumedFood>(mView) {
private val mConsumedFoodName: TextView = mView.consumed_food_name
private val mConsumedKcal: TextView = mView.consumed_kcal
private val mConsumedAmount: TextView = mView.consumed_amount
private val mConsumedCarbs: TextView = mView.consumed_carb_amount
private val mConsumedProtein: TextView = mView.consumed_protein_amount
private val mConsumedFat: TextView = mView.consumed_fat_amount
override fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
object : ItemDetailsLookup.ItemDetails<Long>() {
override fun getSelectionKey(): Long? = itemId
override fun getPosition(): Int = adapterPosition
}
fun bind(isActivated: Boolean = false, item: ConsumedFood) {
val consumedMacroOfFood = calculateConsumedMacro(item.amount,item.consumedFood)
itemView.isActivated = isActivated
mConsumedFoodName.text = item.consumedFood
mConsumedKcal.text = itemView.context.getString(R.string.list_item_kcal,consumedMacroOfFood[0].toInt())
mConsumedAmount.text = itemView.context.getString(R.string.list_item_amount,item.amount)
mConsumedCarbs.text = itemView.context.getString(R.string.list_item_macro,consumedMacroOfFood[1])
mConsumedProtein.text = itemView.context.getString(R.string.list_item_macro,consumedMacroOfFood[2])
mConsumedFat.text = itemView.context.getString(R.string.list_item_macro,consumedMacroOfFood[3])
}
}
inner class DateViewHolder(mView: View): BaseViewHolder<ConsumedFood>(mView) {
private val mConsumedDate: TextView = mView.food_diary_date
private val mTotalConsumedDayKcal: TextView = mView.food_diary_date_sum_kcal
private val mTotalConsumedDayCarbs: TextView = mView.food_diary_date_sum_carbs
private val mTotalConsumedDayProtein: TextView = mView.food_diary_date_sum_protein
private val mTotalConsumedDayFat: TextView = mView.food_diary_date_sum_fat
override fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
object : ItemDetailsLookup.ItemDetails<Long>() {
override fun getSelectionKey(): Long? = itemId
override fun getPosition(): Int = adapterPosition
}
fun bind(item: ConsumedFood) {
mConsumedDate.text = itemView.context
.getString(R.string.list_item_date,DateFormat.getDateInstance(DateFormat.MEDIUM,DateFormat.getAvailableLocales()[0]).format(item.consumedDate))
mTotalConsumedDayKcal.text = itemView.context.getString(R.string.list_item_kcal,getDailyValues(item.consumedDate)[0].toInt())
mTotalConsumedDayCarbs.text = itemView.context.getString(R.string.list_item_macro,getDailyValues(item.consumedDate)[1])
mTotalConsumedDayProtein.text = itemView.context.getString(R.string.list_item_macro,getDailyValues(item.consumedDate)[2])
mTotalConsumedDayFat.text = itemView.context.getString(R.string.list_item_macro,getDailyValues(item.consumedDate)[3])
}
}
override fun getItemViewType(position: Int): Int {
return if(mConsumedFoodList[position].amount < 0) {
DATE_VIEW_HOLDER
} else CONSUMED_FOOD_VIEW_HOLDER
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<ConsumedFood> {
return when(viewType) {
DATE_VIEW_HOLDER -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_food_diary_date_list_item,parent,false)
DateViewHolder(view)
}
//CONSUMED_FOOD_VIEW_HOLDER
else -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_food_diary_list_item,parent,false)
ConsumedFoodViewHolder(view)
}
}
}
override fun getItemCount(): Int = mConsumedFoodList.size
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
when(holder) {
is DateViewHolder -> {
tracker?.let {
holder.bind(mConsumedFoodList[position])
}
}
is ConsumedFoodViewHolder -> {
tracker?.let {
holder.bind(it.isSelected(position.toLong()),mConsumedFoodList[position])
}
}
}
}
override fun getItemId(position: Int): Long = position.toLong()
internal fun getFood(ids: Selection<Long>): List<ConsumedFood> {
val foodsToDelete: MutableList<ConsumedFood> = mutableListOf()
ids.forEach {
foodsToDelete.add(mConsumedFoodList[it.toInt()])
}
return foodsToDelete.toList()
}
internal fun setConsumedFood(consumedFood: List<ConsumedFood>) {
mConsumedFoodList = consumedFood
notifyDataSetChanged()
}
internal fun setFood(food: List<Food>) {
mFoodList = food
notifyDataSetChanged()
}
private fun calculateConsumedMacro(amount: Int, food: String): List<Double>{
val kcalOfTheFood = mFoodList.firstOrNull{ it.foodName == food}?.kcal?.toDouble() ?: 0.0
val carbsOfTheFood = mFoodList.firstOrNull{ it.foodName == food}?.carbs ?: 0.0
val proteinOfTheFood = mFoodList.firstOrNull{ it.foodName == food}?.protein ?: 0.0
val fatOfTheFood = mFoodList.firstOrNull{ it.foodName == food}?.fat ?: 0.0
return listOf(kcalOfTheFood.getAmountOfMacro(amount),
carbsOfTheFood.getAmountOfMacro(amount),
proteinOfTheFood.getAmountOfMacro(amount),
fatOfTheFood.getAmountOfMacro(amount))
}
private fun Double.getAmountOfMacro(amount: Int): Double {
return this * amount / 100
}
private fun getDailyValues(day: Date): List<Double> {
val foodsOfSameDay = mConsumedFoodList.filter{ DateFormat.getDateInstance().format(it.consumedDate) == DateFormat.getDateInstance().format(day)}
val macrosOfSameDay = mutableListOf<List<Double>>()
foodsOfSameDay.forEach {macrosOfSameDay.add(calculateConsumedMacro(it.amount,it.consumedFood))}
val sumKcal = macrosOfSameDay.sumByDouble { it[0] }
val sumCarbs = macrosOfSameDay.sumByDouble { it[1] }
val sumProtein = macrosOfSameDay.sumByDouble { it[2] }
val sumFat = macrosOfSameDay.sumByDouble { it[3] }
return listOf(sumKcal,sumCarbs,sumProtein,sumFat)
}
}
Error
2020-02-18 18:34:07.287 28614-28614/com.hooni.macrotracker E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.hooni.macrotracker, PID: 28614
java.lang.NullPointerException: Attempt to invoke virtual method 'int androidx.recyclerview.widget.RecyclerView$ViewHolder.getAdapterPosition()' on a null object reference
at androidx.recyclerview.selection.StableIdKeyProvider.onDetached(StableIdKeyProvider.java:90)
at androidx.recyclerview.selection.StableIdKeyProvider$1.onChildViewDetachedFromWindow(StableIdKeyProvider.java:69)
at androidx.recyclerview.widget.RecyclerView.dispatchChildDetached(RecyclerView.java:7546)
at androidx.recyclerview.widget.RecyclerView.removeDetachedView(RecyclerView.java:4349)
at androidx.recyclerview.widget.RecyclerView$Recycler.getScrapOrCachedViewForId(RecyclerView.java:6738)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6189)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3851)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1915)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1915)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1812)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1656)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1565)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
at com.android.internal.policy.DecorView.onLayout(DecorView.java:753)
at android.view.View.layout(View.java:20672)
at android.view.ViewGroup.layout(ViewGroup.java:6194)
at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2792)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2319)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1460)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7183)
2020-02-18 18:34:07.288 28614-28614/com.hooni.macrotracker E/AndroidRuntime: at android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)
at android.view.Choreographer.doCallbacks(Choreographer.java:761)
at android.view.Choreographer.doFrame(Choreographer.java:696)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
when adding a new item to the recyclerView.

The Problem is related to the SelectrionTracker (RecyclerView Selection, which was just briefly mentioned) and it's StableIdKeyProvider.
So... apparently this crash happens, when the StableIdKeyProvider is trying to removed/replaced the stable id while the item (cardview) is no longer attached to the ViewHolder (DateViewHolder or ConsumedFoodViewHolder).
Honestly, I still do not understand the logic behind that, but once I will, I will come back and update this post.
The solution is to provide a custom KeyProvider like the following one:
class ConsumedFoodListItemKeyProvider(private val recyclerView: RecyclerView) : ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_MAPPED) {
override fun getKey(position: Int): Long? {
return recyclerView.adapter?.getItemId(position)
}
override fun getPosition(key: Long): Int {
val viewHolder = recyclerView.findViewHolderForItemId(key)
return viewHolder?.layoutPosition ?: RecyclerView.NO_POSITION
}
}
and use it in the SelectionTracker.Builder

Related

error: No value passed for parameter 'priority

error: No value passed for parameter 'priority
i went through my code and cant see why im getting the error , i passed the value for it but not sure why its not recognizing it.
error in EditNotesFragment.kt
No value passed for parameter 'priority'
error in CreateNotesFragment.kt
No value passed for parameter 'priority'
error in NotesAdapter.kt
Unresolved reference: viewPriority
EditNotesFragment.kt
package com.cameron.armymaintenance.ui.fragments
import android.os.Bundle
import android.text.format.DateFormat
import android.view.*
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.navigation.fragment.navArgs
import com.cameron.armymaintenance.R
import com.cameron.armymaintenance.ViewModel.NotesViewModel
import com.cameron.armymaintenance.databinding.FragmentEditNotesBinding
import com.cameron.armymaintenance.model.Notes
import com.google.android.material.bottomsheet.BottomSheetDialog
import java.util.*
class EditNotesFragment : Fragment() {
val oldNotes by navArgs<EditNotesFragmentArgs>()
lateinit var binding: FragmentEditNotesBinding
val viewModel : NotesViewModel by viewModels()
var priority:String = "1"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
binding= FragmentEditNotesBinding.inflate(layoutInflater,container,false)
setHasOptionsMenu(true)
binding.edtBumperNumber.setText(oldNotes.data.bumperNumber)
binding.edtEquipmentType.setText(oldNotes.data.equipmentType)
binding.edtModelType.setText(oldNotes.data.modelType)
binding.edtSerialNumber.setText(oldNotes.data.serialNumber)
binding.edtFaultDescription.setText(oldNotes.data.faultDescription)
binding.edtServiceDate.setText(oldNotes.data.serviceDate)
if(oldNotes.data.priority == "1"){
priority="1"
binding.fmc.setImageResource(R.drawable.baseline_check_24)
binding.nmc.setImageResource(0)
binding.fault.setImageResource(0)
}else if(oldNotes.data.priority == "2"){
priority="2"
binding.fault.setImageResource(R.drawable.baseline_check_24)
binding.nmc.setImageResource(0)
binding.fmc.setImageResource(0)
}else{
priority="3"
binding.nmc.setImageResource(R.drawable.baseline_check_24)
binding.fmc.setImageResource(0)
binding.fault.setImageResource(0)
}
binding.fmc.setOnClickListener{
binding.fmc.setImageResource(R.drawable.baseline_check_24)
binding.nmc.setImageResource(0)
binding.fault.setImageResource(0)
priority="1"
}
binding.fault.setOnClickListener{
binding.fault.setImageResource(R.drawable.baseline_check_24)
binding.nmc.setImageResource(0)
binding.fmc.setImageResource(0)
priority="2"
}
binding.nmc.setOnClickListener{
binding.nmc.setImageResource(R.drawable.baseline_check_24)
binding.fmc.setImageResource(0)
binding.fault.setImageResource(0)
priority="3"
}
binding.btnEditSaveNotes.setOnClickListener {
updateNotes(it)
}
return binding.root
}
private fun updateNotes(it: View?) {
val bumperNumber = binding.edtBumperNumber.text.toString()
val equipmentType = binding.edtEquipmentType.text.toString()
val modelType = binding.edtModelType.text.toString()
val serialNumber = binding.edtSerialNumber.text.toString()
val serviceDate = binding.edtServiceDate.text.toString()
val faultDescription = binding.edtFaultDescription.text.toString()
val d = Date()
val notesDate: CharSequence = DateFormat.format("MMMM d, yyyy ", d.time)
// we gotta update id on clicking
val data = Notes(oldNotes.data.id,bumperNumber,equipmentType,modelType,serialNumber,serviceDate,faultDescription,notesDate.toString(),priority)
viewModel.updateNotes(data)
Toast.makeText(requireContext(),"Note updated successfully", Toast.LENGTH_SHORT).show()
Navigation.findNavController(it!!).navigate(R.id.action_editNotesFragment_to_homeFragment)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.delete_menu,menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if(item.itemId==R.id.menu_delete){
val bottomSheet: BottomSheetDialog = BottomSheetDialog(requireContext(),R.style.BottomSheetStyle)
bottomSheet.setContentView(R.layout.dialog_delete)
val textViewYes=bottomSheet.findViewById<TextView>(R.id.dialog_yes)
val textViewNo=bottomSheet.findViewById<TextView>(R.id.dialog_no)
textViewYes?.setOnClickListener {
viewModel.deleteNotes(oldNotes.data.id!!)
bottomSheet.dismiss()
Navigation.findNavController(binding.btnEditSaveNotes).navigate(R.id.action_editNotesFragment_to_homeFragment)
}
textViewNo?.setOnClickListener {
bottomSheet.dismiss()
}
bottomSheet.show()
}
return super.onOptionsItemSelected(item)
}
}
CreateNotesFragment.kt
package com.cameron.armymaintenance.ui.fragments
import android.os.Bundle
import android.text.format.DateFormat
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import com.cameron.armymaintenance.model.Notes
import com.cameron.armymaintenance.R
import com.cameron.armymaintenance.ViewModel.NotesViewModel
import com.cameron.armymaintenance.databinding.FragmentCreateNotesBinding
import java.util.*
class CreateNotesFragment : Fragment() {
private lateinit var binding: FragmentCreateNotesBinding
private var priority : String = "1"
private val viewModel : NotesViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
binding = FragmentCreateNotesBinding.inflate(layoutInflater,container,false)
binding.fmc.setImageResource(R.drawable.baseline_check_24)
binding.fmc.setOnClickListener{
binding.fmc.setImageResource(R.drawable.baseline_check_24)
binding.fault.setImageResource(0)
binding.nmc.setImageResource(0)
priority="1"
}
binding.fault.setOnClickListener{
binding.fault.setImageResource(R.drawable.baseline_check_24)
binding.fmc.setImageResource(0)
binding.nmc.setImageResource(0)
priority="2"
}
binding.nmc.setOnClickListener{
binding.nmc.setImageResource(R.drawable.baseline_check_24)
binding.fmc.setImageResource(0)
binding.fault.setImageResource(0)
priority="3"
}
binding.btnSaveNotes.setOnClickListener {
createNotes(it)
}
return binding.root
}
private fun createNotes(it: View?) {
val bumperNumber = binding.edtBumperNumber.text.toString()
val equipmentType = binding.edtEquipmentType.text.toString()
val modelType = binding.edtModel.text.toString()
val serialNumber = binding.edtSerialNumber.text.toString()
val serviceDate = binding.edtServiceDate.text.toString()
val faultDescription = binding.edtFaultDescription.text.toString()
val d = Date()
val notesDate: CharSequence = DateFormat.format("MMMM d, yyyy ", d.time)
val data = Notes(null,bumperNumber,equipmentType,modelType,serialNumber,serviceDate,faultDescription,notesDate.toString(),priority)
viewModel.addNotes(data)
Toast.makeText(requireContext(),"Notes created successfully",Toast.LENGTH_SHORT).show()
Navigation.findNavController(it!!).navigate(R.id.action_createNotesFragment_to_homeFragment)
}
}
NotesAdapter.kt
package com.cameron.armymaintenance.ui.Adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import com.cameron.armymaintenance.R
import com.cameron.armymaintenance.databinding.ItemNotesBinding
import com.cameron.armymaintenance.model.Notes
import com.cameron.armymaintenance.ui.fragments.HomeFragmentDirections
class NotesAdapter(val requireContext: Context, var notesList: List<Notes>) : RecyclerView.Adapter<NotesAdapter.notesViewHolder>() {
//Contructor
//search ke liye filter
fun filtering(newFilteredList:ArrayList<Notes>){
notesList=newFilteredList
notifyDataSetChanged()
}
class notesViewHolder(val binding : ItemNotesBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): notesViewHolder {
return notesViewHolder(
ItemNotesBinding.inflate(
LayoutInflater.from(parent.context),parent,false))
}
override fun onBindViewHolder(holder: notesViewHolder, position: Int) {
holder.binding.notesBumperNumber.text = notesList[position].bumperNumber
holder.binding.notesEquipmentType.text = notesList[position].equipmentType
holder.binding.notesModelType.text = notesList[position].modelType
holder.binding.notesSerialNumber.text = notesList[position].serialNumber
holder.binding.notesFaultDescription.text = notesList[position].faultsDescription
holder.binding.notesServiceDate.text = notesList[position].serviceDate
holder.binding.notesDate.text = notesList[position].date
if(notesList[position].priority == "1"){
holder.binding.viewPriority.setBackgroundResource(R.drawable.green_dot)
}else if(notesList[position].priority == "2"){
holder.binding.viewPriority.setBackgroundResource(R.drawable.yellow_dot)
}else{
holder.binding.viewPriority.setBackgroundResource(R.drawable.red_dot)
}
holder.binding.root.setOnClickListener {
val action = HomeFragmentDirections.actionHomeFragmentToEditNotesFragment(notesList[position])
Navigation.findNavController(it).navigate(action)
}
}
override fun getItemCount(): Int {
return notesList.size
}
}
Notes.kt
package com.cameron.armymaintenance.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
#Parcelize
#Entity(tableName = "Notes")
class Notes(
#PrimaryKey(autoGenerate = true)
var id:Int?=null,
var bumperNumber: String,
var equipmentType: String,
var modelType: String,
var serialNumber: String,
var faultsDescription: String,
var serviceDate: String,
var notes: String,
var date: String,
var priority: String
): Parcelable

How to casting this Generic class for two layouts in Kotlin?

package com.hypermarket.ui.main.adapter
import android.graphics.Paint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
class ProductsGridAdapter(
fragment: Fragment,
val context: MainActivity,
private val myPref: PreferencesManager,
screenName: ScreenType,
productInterface: ProductInterface,
rv: RecyclerView,
isFromDetails: Boolean,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items = arrayListOf<ProductModel>()
var parentFragment: Fragment = fragment
var bottomSheetDialog: BottomSheetDialog? = null
var mAdapter: ProductsGridAdapter? = null
var screenType: ScreenType
var productInterface: ProductInterface = productInterface
private var loading = false
private var onLoadMoreListener: OnLoadMoreListener? = null
private var recyclerView: RecyclerView
private var isFromDetails: Boolean? = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
if (isFromDetails == false) {
val itemBinding: RowItemProductGridBinding =
RowItemProductGridBinding.inflate(layoutInflater, parent, false)
//itemBinding.productInterface = productInterface
return ViewHolder(itemBinding)
} else {
val itemBinding: RowItemSimilarProductBinding =
RowItemSimilarProductBinding.inflate(layoutInflater, parent, false)
//itemBinding.productInterface = productInterface
return ViewHolder(itemBinding)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (isFromDetails == true) {
(holder as ViewHolder<RowItemSimilarProductBinding>).bind(position)
} else {
(holder as ViewHolder<RowItemProductGridBinding>).bind(position)
}
}
fun <T> getCurrentView(binding: T): View {
if (isFromDetails == true) {
return (binding as RowItemSimilarProductBinding).root
} else {
return (binding as RowItemProductGridBinding).root
}
}
fun <T> getItemSimilarProductBinding(binding: T): RowItemSimilarProductBinding {
return binding as RowItemSimilarProductBinding
}
fun <T> getItemProductGridBinding(binding: T): RowItemProductGridBinding {
return binding as RowItemProductGridBinding
}
inner class ViewHolder<T>(bindings: T) : RecyclerView.ViewHolder(getCurrentView(bindings)) {
private val binding: T = bindings
fun bind(position: Int) {
val model: ProductModel = items[position]
if (isFromDetails == true) {
getItemSimilarProductBinding(binding).item = model
} else {
getItemProductGridBinding(binding).item = model
}
binding.tvMRP.paintFlags = binding.tvMRP.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
binding.executePendingBindings()
binding.ivProduct.setBackgroundResource(0)
if (model.weight_based == Constants.WEIGHT_BASED) {
binding.parentView.disabled =
AppValidator.toFloat(model.available_qty_weight_based) < AppValidator.toFloat(
model.gram_variant_en)
} else {
binding.parentView.disabled = AppValidator.toInt(model.available_qty) == 0
}
binding.ivType.setGone()
}
}
Here I want to use two different layout (Which you can see on onCreateViewHolder() method). two different layout has all the property named with one another. only design is different. now I want to use the layout based on isFromDetails (Which is boolean value). based on that, I want access the both layout bindings with single variable. is it possible?
private val binding: T = bindings
fun bind(position: Int) {
val model: ProductModel = items[position]
if (isFromDetails == true) {
getItemSimilarProductBinding(binding).item = model
} else {
getItemProductGridBinding(binding).item = model
}
binding.tvMRP.paintFlags = binding.tvMRP.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
binding.ivType.setGone()
}
here both layout has tvMRP and ivType. Is it possible without adding boiler plate code?

The RecyclerView scrolls to downward when a new message is added

I have a chat app. I used RecyclerView and I set stackFromEnd true and reverseLayout false to LinearlayoutManager. when a new message is added at the bottom I mean at the end of the list the recyclerView starts auto scroll downward instead of upward. the adapter is notified by notifyItemInserted().
Expected Behaviour: When a new message is added to the list it should scroll to the bottom or upward. Any help is appreciated. Thanks,
Here is adapter:
class ChatMessagesListAdapter(
var chatMessages: ChatMessagesDataModel
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
// we set the header as 0 so we can add more types to the ConversationItem enum
private const val ITEM_TYPE_HEADER = 0
}
override fun getItemViewType(position: Int): Int {
return when {
chatMessages.hasMoreItems -> chatMessages.messages[position].getItemType()
position == 0 -> ITEM_TYPE_HEADER
else -> chatMessages.messages[position - 1].getItemType()
}
}
// return +1 to draw the header if there are no items to load
override fun getItemCount() = chatMessages.messages.size + getContentIndex()
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ITEM_TYPE_HEADER -> {
MessagesHeaderViewHolder(inflater.inflate(R.layout.item_chat_messages_header,
parent, false))
}
ChatItemDataModel.TYPE_ADDED_USER,
ChatItemDataModel.TYPE_VIDEO_NOTIFICATION -> {
MessagesInfoViewHolder(inflater.inflate(R.layout.item_chat_messages_info,
parent, false))
}
else -> {
MessagesMessageViewHolder(inflater.inflate(R.layout.item_chat_messages_message,
parent, false))
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as? MessagesHeaderViewHolder)?.bind()
(holder as? MessagesMessageViewHolder)?.bind(position, chatMessages.messages[getRealPosition(position)])
(holder as? MessagesInfoViewHolder)?.bind(chatMessages.messages[getRealPosition(position)])
}
private fun getContentIndex() = if (chatMessages.hasMoreItems) {
0
} else {
1
}
private fun getRealPosition(position: Int) = if (chatMessages.hasMoreItems) {
position
} else {
position - 1
}
private fun notifyChanges() {
if (chatMessages.numberOfItemsInserted == chatMessages.messages.size || isPreview ||
chatMessages.hadPendingMessages) {
notifyDataSetChanged()
} else {
// +1 because of the header
notifyItemRangeInserted(chatMessages.insertionIndex + getContentIndex(),
chatMessages.numberOfItemsInserted)
}
}
fun updateConversation(newChatMessages: ChatMessagesDataModel) {
chatMessages = newChatMessages
notifyChanges()
}
fun updateMessage(newMessage: ChatItemDataModel, isRemote: Boolean) {
if (newMessage.message.hashIdentifier.isNullOrBlank()) {
addNewMessage(newMessage)
return
}
val messageIndex = chatMessages.messages.indexOfFirst { it.message.hashIdentifier == newMessage.message.hashIdentifier }
if (messageIndex != -1) {
val localMessage = chatMessages.messages[messageIndex]
chatMessages.messages[messageIndex] = newMessage
if (failedMessages.contains(localMessage.message.sid)) {
if (isRemote) {
failedMessages.remove(localMessage.message.sid)
}
notifyItemChanged(messageIndex + getContentIndex())
}
}
else {
addNewMessage(newMessage)
}
}
private fun addNewMessage(newMessage: ChatItemDataModel) {
val oldCount = chatMessages.messages.size
chatMessages.messages.add(newMessage)
notifyItemInserted(oldCount + getContentIndex())
}
fun addLocalMessage(
sharedPrefsStorage: SharedPrefsStorage,
message: String, imageUri: Uri?, hashIdentifier: String
) {
val userMessage = UserMessage(messageBody = message, firstName = sharedPrefsStorage.firstName,
lastName = sharedPrefsStorage.lastName, isFromLoggedUser = true, imageUri = imageUri,
hashIdentifier = hashIdentifier, files = null, reactions = null)
val newMessage = ChatItemDataModel(userMessage, sharedPrefsStorage.profileImageUrl, sharedPrefsStorage.userId.toString())
val oldCount = chatMessages.messages.size
chatMessages.messages.add(newMessage)
notifyItemRangeInserted(oldCount + getContentIndex(), 1)
}
....
}
Here is Fragment:
class ChatRoomMessagesFragment : Fragment() {
#Inject
lateinit var sharedPrefsStorage: SharedPrefsStorage
private var adapter: ChatMessagesListAdapter? = null
......
override fun onMessagesReceived(chatMessages: ChatMessagesDataModel) {
if (adapter == null) {
adapter = ChatMessagesListAdapter(chatMessages)
adapter?.setHasStableIds(true)
binding.recyclerview.adapter = adapter
} else {
adapter?.updateConversation(chatMessages)
}
}
override fun onUserMessageRetrieved(newMessage: ChatItemDataModel, isRemote: Boolean) {
adapter?.updateMessage(newMessage, isRemote)
}
private fun setupUI() {
binding.apply {
recyclerview.apply {
layoutManager = LinearLayoutManager(requireContext()).apply {
reverseLayout = false
stackFromEnd = true
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
showScrollToBottomButtonIfNeeded(findFirstCompletelyVisibleItemPosition())
}
})
}
addOnScrollListener(object : PaginationScrollListener(
layoutManager as LinearLayoutManager,
ScrollDirection.BOTTOM
) {
override fun loadMoreItems() {
presenter.loadMoreMessages()
}
override fun isLastPage() = !presenter.hasMoreItems()
override fun isLoading() = presenter.isLoadingInProgress()
})
// prevent the list from scrolling when the keyboard opens
addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
if (bottom < oldBottom) {
scrollChatToBottom()
}
}
}
....
adapter?.addLocalMessage(sharedPrefsStorage, message, imageUri, hashIdentifier)
presenter.sendMessage("", message, imageUri, hashIdentifier)
}
private fun scrollChatToBottom() {
binding.apply {
recyclerview.post {
recyclerview.smoothScrollToPosition(recyclerview.adapter?.itemCount ?: 0)
}
}
}
}
Let's suppose your adapter is populated by objects called messages and you have a source of data called messageList (could be a Collection, etc) that is passed to your adapter. You could do something like this:
int position = messageList.size() > 0 ? (messageList.size()-1) : 0;
mRecyclerView.smoothScrollToPosition(position);
Just ensure your adapter is not null and actually gets data from messageList since this is in your Activity/Fragment class and not in the adapter class.
You can scrolldown progrmatically using code below
recyclerView.scrollToPosition(chatList.size() - 1);
You can also try another solution if above one doesn't works
setStackFromEnd(true) or setReverseLayout(true)
yourAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
this#EventDetails.binding.rvData.scrollToPosition(positionStart)
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
this#EventDetails.binding.rvData.scrollToPosition(positionStart)
}
})
one of these method will work i think 1st one, try
and
try to avoid notifyDataSetChange() or intemChange() call by your self use DiffUtils or ListAdapter(Extension of recycler view)

Pagination Duplicating Values in RecyclerView in MVVM

I am new in Kotlin MVVM also, I tried to achieved Pagination with legacy approach and stucked in a issue with my RecyclerView, whenever I scroll it the data duplicated, I tried DiffUtils but no help.
I Logged the data in VIEWMODEL class the data is not repeating
but, when I logged in Activity where I am observing it is showing duplicate values
SEARCHRESULTACTIVITY.KT
class SearchResultActivity : AppCompatActivity() {
private lateinit var layoutManager: LinearLayoutManager
private lateinit var recyclerView: RecyclerView
private lateinit var pullAdapter: CustomAdapter
private var pageNumber = 1
private var totalItemsCount = 0
private var firstVisibleItemsCount = 0
private var visibleItemsCount = 0
private var previousTotal = 0
private var loading = true
private var fillPullList: ArrayList<RepoPull> = ArrayList()
private var userName: String = ""
private var repoName: String = ""
private var isEnd = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initialize()
getDataPull(userName, repoName)
loadNextData()
}
private fun initialize() {
setContentView(R.layout.activity_search_result)
recyclerView = findViewById(R.id.repoRecView)
layoutManager = LinearLayoutManager(this)
getSearchQuery()
}
private fun getSearchQuery() {
userName = intent.getStringExtra("owner").toString()
repoName = intent.getStringExtra("repo").toString()
populatePullRv()
}
private fun populatePullRv() {
recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
recyclerView.layoutManager = layoutManager
pullAdapter = CustomAdapter(this, fillPullList)
recyclerView.adapter = pullAdapter
progressBar.visibility = View.VISIBLE
}
private fun loadNextData() {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val mLayoutManger = recyclerView.layoutManager as LinearLayoutManager
visibleItemsCount = mLayoutManger.childCount
totalItemsCount = mLayoutManger.itemCount
firstVisibleItemsCount = mLayoutManger.findFirstVisibleItemPosition()
if (loading) {
if (totalItemsCount > previousTotal) {
previousTotal = totalItemsCount
pageNumber++
loading = false
progressBar.visibility = View.GONE
}
}
if (!loading && (firstVisibleItemsCount + visibleItemsCount) >= totalItemsCount) {
getDataPull(userName, repoName)
loading = true
Log.d("PAGE", pageNumber.toString())
}
}
})
}
private fun getDataPull(username: String?, reponame: String?) {
val myViewModel = ViewModelProviders.of(this).get(PullVM::class.java)
myViewModel.endofList.observe(this, {
if (it == true) {
isEnd = true
progressBar.visibility = View.GONE
Toast.makeText(this#SearchResultActivity, "All PR Fetched", Toast.LENGTH_SHORT)
.show()
}
})
myViewModel.status.observe(this, {
if (it == false) {
showError(getString(R.string.no_net))
}
})
myViewModel.getPullDataFromVM().observe(this, {
if (it != null) {
listRepos(it) **//DUPLICATE VALUE COMING**
} else {
showError(getString(R.string.nothing_found))
}
})
myViewModel.getPullList(username.toString(), reponame.toString(), pageNumber)
}
private fun showError(s: String) {
progressBar.visibility = View.GONE
val theView =
this#SearchResultActivity.findViewById<View>(android.R.id.content)
Snackbar.make(
theView,
s,
Snackbar.LENGTH_LONG
).show()
}
#SuppressLint("NotifyDataSetChanged")
fun listRepos(repos: List<RepoPull>) {
if (!isEnd) {
progressBar.visibility = View.GONE
fillPullList.addAll(repos)
pullAdapter.notifyDataSetChanged()
}
}}
PULLVM(View Model).kt
class PullVM : ViewModel() {
var pullList: MutableLiveData<List<RepoPull>>
var status = MutableLiveData<Boolean?>()
var endofList = MutableLiveData<Boolean?>()
init {
pullList = MutableLiveData()
}
fun getPullDataFromVM(): MutableLiveData<List<RepoPull>> {
return pullList
}
fun getPullList(ownerName: String, repoName: String, pgNo: Int) {
val retriever = GitHubRetriever
val callback = object : Callback<List<RepoPull>> {
override fun onFailure(call: Call<List<RepoPull>>, t: Throwable) {
status.value = false
}
override fun onResponse(
call: Call<List<RepoPull>>,
response: Response<List<RepoPull>>
) {
if (response.body()?.size == 0) {
endofList.value = true
}
if (response.code() == 404) {
pullList.postValue(null)
} else {
status.value = true
val repos = response.body()
if (repos != null) {
pullList.postValue(repos)
}
}
}
}
retriever.userRepos(
callback,
ownerName,
repoName,
pgNo
)
}
Try moving your viewModel instantiation and observer settings to onCreate so you don't have to create a new viewModel instance and set a new observable to your LiveDatas.
Declare myViewModel as a lateinit property of your Activity and move this part to onCreate
myViewModel = ViewModelProviders.of(this).get(PullVM::class.java)
myViewModel.endofList.observe(this, {
if (it == true) {
isEnd = true
progressBar.visibility = View.GONE
Toast.makeText(this#SearchResultActivity, "All PR Fetched", Toast.LENGTH_SHORT)
.show()
}
})
myViewModel.status.observe(this, {
if (it == false) {
showError(getString(R.string.no_net))
}
})
myViewModel.getPullDataFromVM().observe(this, {
if (it != null) {
listRepos(it) **//DUPLICATE VALUE COMING**
} else {
showError(getString(R.string.nothing_found))
}
})
And
private fun getDataPull(username: String?, reponame: String?)
should only contain
myViewModel.getPullList(username.toString(), reponame.toString(), pageNumber)

notifyDataSetChanged does not work in adapter in kotlin

There is a delete button on my recylerview item when I click it I m showing a alert dialog when I click yes button I want to refresh my recylerview but I could'not. I call directly notifyDataSetChanged in adapter at the end of the my code but it does not work.
MyAlertDialog(In my adapter)
mAlertDialog.setPositiveButton("Yes") { dialog, id ->
val databaseHandler: DBHelper = DBHelper(holder.itemView.context)
val removedMed = Medicine(id = medicine.id,name = medicine.name,amount = medicine.amount,description = medicine.description)
databaseHandler.deleteMedicine(removedMed)
dialog.dismiss()
notifyDataSetChanged()
}
Mainactivity
class MainActivity : AppCompatActivity() {
private var adapter: MedicineAdapter? = null
private var medicineList : ArrayList<Medicine> = arrayListOf()
private var recyclerView: RecyclerView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getDataFromDb()
}
fun getDataFromDb(){
val context = this
val db = DBHelper(context)
recyclerView = findViewById<View>(R.id.recyclerView) as RecyclerView
adapter = MedicineAdapter(this, db.readData())
val layoutManager = LinearLayoutManager(applicationContext)
recyclerView!!.layoutManager = layoutManager
recyclerView!!.itemAnimator = DefaultItemAnimator()
// set the adapter
recyclerView!!.adapter = adapter
adapter!!.notifyDataSetChanged()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.addBtn -> {
val intent = Intent(this,AddNewMedicine::class.java)
startActivity(intent)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onResume() {
super.onResume()
getDataFromDb()
}
}
MedicineAdapter
class MedicineAdapter(
private val mainActivity: MainActivity,
val medicineList: ArrayList<Medicine>)
: RecyclerView.Adapter<MedicineAdapter.ListItemHolder>(){
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int): ListItemHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.medicine_items, parent, false)
return ListItemHolder(itemView)
}
inner class ListItemHolder(view: View) :
RecyclerView.ViewHolder(view),
View.OnClickListener {
internal var name = view.findViewById<TextView>(R.id.name)
internal var amount = view.findViewById<TextView>(R.id.amount)
internal var description = view.findViewById<TextView>(R.id.description)
internal var deleteButton = view.findViewById<ImageButton>(R.id.deleteBtn)
internal var editButton = view.findViewById<ImageButton>(R.id.editBtn)
init {
view.isClickable = true
view.setOnClickListener(this)
deleteButton.setOnClickListener(this)
editButton.setOnClickListener(this)
}
override fun onClick(view: View) {
//val intentToCarPager = Intent(view!!.context, CarPagerActivity::class.java)
//view.context.startActivity(intentToCarPager)
}
}
override fun onBindViewHolder(holder: ListItemHolder, position: Int) {
val medicine = medicineList!![position]
holder.name.text = medicine.name
holder.amount.text = medicine.amount
holder.description.text = medicine.description
holder.deleteButton.setOnClickListener {
val mAlertDialog = AlertDialog.Builder(holder.itemView.context)
mAlertDialog.setTitle("Are you sure you want to this record!") //set alertdialog title
mAlertDialog.setPositiveButton("Yes") { dialog, id ->
val databaseHandler: DBHelper = DBHelper(holder.itemView.context)
val removedMed = Medicine(id = medicine.id,name = medicine.name,amount = medicine.amount,description = medicine.description)
databaseHandler.deleteMedicine(removedMed)
notifyDataSetChanged()
dialog.dismiss()
medicineList.removeAt(position)
notifyItemRemoved(position)
}
mAlertDialog.setNegativeButton("No") { dialog, id ->
}
mAlertDialog.show()
}
holder.editButton.setOnClickListener {
val dialog = UpdateMedicine(medicine)
val manager = (holder.itemView.context as MainActivity).supportFragmentManager
dialog.show(manager,"")
}
}
override fun getItemCount(): Int {
if (medicineList != null) {
return medicineList.size
}
return -1
}
}
I solved my problem by removing the object from the array
notifyDataSetChanged()
dialog.dismiss()
medicineList.removeAt(position)
notifyItemRemoved(position)

Categories