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

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?

Related

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)

List/grid switchable recyclerview doesn't work

I'm trying to create a recyclerview which can change layout (a list or a grid), for that I modified my adapter to determine the which layout it should use according to the number of spans. But when I try to modify the information from the holder nothing is happening. Does anyone have an idea?
here's my adapter:
class FileAdapter(
private val context: Context,
private val layoutManager: GridLayoutManager,
private var items:MutableList<File>,
private val isFolder:Boolean = false
) : RecyclerView.Adapter<FileAdapter.FileViewHolder>() {
companion object {
private val LIST = 1
private val GRID = 3
private val VIEW_TYPE_LIST = 1
private val VIEW_TYPE_GRID = 2
}
override fun getItemViewType(position: Int): Int {
val spanCount: Int = layoutManager.spanCount
return if (spanCount == LIST) {
VIEW_TYPE_LIST
} else {
VIEW_TYPE_GRID
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType : Int):FileViewHolder{
val view : View
if(!isFolder) {
if (viewType == VIEW_TYPE_GRID) {
view = LayoutInflater.from(parent.context).inflate(R.layout.file_element, parent, false)
} else {
view = LayoutInflater.from(parent.context).inflate(R.layout.file_list_element, parent, false)
}
} else
view = LayoutInflater.from(parent.context).inflate(R.layout.folder_element, parent, false)
return FileViewHolder(view, viewType)
}
class FileViewHolder(view : View, viewType: Int) : RecyclerView.ViewHolder(view) {
var fileName: TextView? = null
var fileImg: ImageView? = null
var fileSize: TextView? = null
init {
if (viewType == VIEW_TYPE_GRID) {
val fileName = view.findViewById<TextView>(R.id.file_name)
var fileImg = view.findViewById<ImageView>(R.id.file_img)
} else {
val fileName = view.findViewById<TextView>(R.id.file_name)
var fileImg = view.findViewById<ImageView>(R.id.file_img)
var fileSize = view.findViewById<TextView>(R.id.file_size)
}
}
}
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
val currentFile = items[position]
holder.fileName?.text = currentFile.name
if(currentFile.isDirectory)
//holder.fileImg.setImageResource(getDrawable(R.drawable.ic_folder))
else if(currentFile.extension=="pdf"){
//holder.fileImg.setImageResource(R.drawable.ic_pdf)
CoroutineScope(Dispatchers.Default).launch {
val bitmapPdf = pdfToBitmap(currentFile)
CoroutineScope(Dispatchers.Main).launch {
holder.fileImg?.setImageBitmap(bitmapPdf)
}
}
}
else if(currentFile.extension=="docx")
holder.fileImg?.setImageResource(R.drawable.ic_docx)
else if(currentFile.extension=="xlsx")
holder.fileImg?.setImageResource(R.drawable.ic_xlsx)
else
holder.fileImg?.setImageResource(R.drawable.ic_file)
}
override fun getItemCount() = items.size
}
Here's the result:

Own background on selected item on my spinner

I use PowerSpinner on my application (https://github.com/skydoves/PowerSpinner).
My problem is that I want to have own background color on selected item. I know how to do this on a normal spinner but can't do this on PowerSpinner.
Material have a sample how to implement a custom adapter but is write on Kotlin and I have no ideea about kotlin.
How is java version for next code?
class MySpinnerAdapter(
powerSpinnerView: PowerSpinnerView) : RecyclerView.Adapter<MySpinnerAdapter.MySpinnerViewHolder>(),
PowerSpinnerInterface<MySpinnerItem> {}
Can you help me to implement to PowerSpinner my own background color?
Thanks in advance!
class Adapter(powerSpinnerView: PowerSpinnerView
) : RecyclerView.Adapter<CategoryAdapter.IconSpinnerViewHolder>(),
PowerSpinnerInterface<Data> {
override var index: Int = powerSpinnerView.selectedIndex
override val spinnerView: PowerSpinnerView = powerSpinnerView
override var onSpinnerItemSelectedListener: OnSpinnerItemSelectedListener<Data>? = null`
private val compoundPadding: Int = 12
private var spinnerItems: MutableList<Data> = arrayListOf()
init {
this.spinnerView.compoundDrawablePadding = compoundPadding
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IconSpinnerViewHolder {
val binding =
ItemDefaultPowerSpinnerLibraryBinding.inflate(
LayoutInflater.from(parent.context), parent,
false
)
return IconSpinnerViewHolder(binding).apply {
binding.root.setOnClickListener {
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }
?: return#setOnClickListener
notifyItemSelected(position)
}
}
}
override fun onBindViewHolder(holder: IconSpinnerViewHolder, position: Int) {
holder.bind(spinnerItems[position], spinnerView)
}
override fun setItems(itemList: List<Data>) {
this.spinnerItems.clear()
this.spinnerItems= itemList.toMutableList()
notifyDataSetChanged()
}
override fun notifyItemSelected(index: Int) {
if (index == 0) return
val item = spinnerItems[index]
val oldIndex = this.index
this.index = index
this.spinnerView.notifyItemSelected(index, item.name)
this.onSpinnerItemSelectedListener?.onItemSelected(
oldIndex = oldIndex,
oldItem = oldIndex.takeIf { it != 0 }?.let { spinnerItems[oldIndex] },
newIndex = index,
newItem = item
)
}
override fun getItemCount() = this.spinnerItems.size
class IconSpinnerViewHolder(private val binding: ItemDefaultPowerSpinnerLibraryBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Data, spinnerView: PowerSpinnerView) {
binding.itemDefaultText.apply {
text = item.name
gravity = spinnerView.gravity
setTextSize(TypedValue.COMPLEX_UNIT_PX, spinnerView.textSize)
setTextColor(spinnerView.currentTextColor)
}
binding.root.setPadding(
spinnerView.paddingLeft,
spinnerView.paddingTop,
spinnerView.paddingRight,
spinnerView.paddingBottom
)
}
}
}

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)

findContainingViewHolder returns null

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

Categories