kotlin recyclerview MVVM error - IndexOutOfBoundsException - android

On recyclerview I'm setting a list, using MVVM where I'm getting an error,
IndexOutOfBoundsException.
code I use for recyclerview adapter is below - onBindViewHolder is where I set articles:List
class MainAdapter(private val context: Context,private val articles:List<Article>) : ListAdapter<Article, MainAdapter.MainViewHolder>(DiffUtil()) {
inner class MainViewHolder(val binding: HomeRecViewBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
val binding = HomeRecViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MainViewHolder(binding)
}
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val articles_ = articles[position]
holder.binding.artical = articles_
}
class DiffUtil : androidx.recyclerview.widget.DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
}
my ViewModel is given below
val article : LiveData<NewsModel>
get() = repoHelper.article
my main Activity
class TestActivity : AppCompatActivity() {
lateinit var binding: ActivityTestBinding
lateinit var mainViewModel: MainViewModel
lateinit var article_: List<Article>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.activity_test)
article_ = ArrayList()
val mainAdapter = MainAdapter(this,article_)
binding.recView.apply {
this.layoutManager = LinearLayoutManager(this#TestActivity)
this.adapter = mainAdapter
}
val apiHelper = RetrofitHelper.getInstance().create(ApiHelper::class.java)
val database_ = DBHelper.getDatabase(applicationContext)
val repoHelper = RepoHelper(apiHelper,database_,applicationContext)
mainViewModel = ViewModelProvider(this,ViewModelFactory(repoHelper)).get(MainViewModel::class.java)
mainViewModel.article.observe(this, Observer {
Log.d("list_of_article",it.articles.toString())
mainAdapter.submitList(it.articles)
})
}
what's the main cause for this error and how do I resolve it.

article_ = ArrayList()
val mainAdapter = MainAdapter(this,article_)
Here you provide an empty list to the Adapter.
After this never changes!
mainAdapter.submitList(it.articles) does not set this list in the adapter.
So val articles_ = articles[position] will try to access something from an empty list.
As a matter of fact. You don't even need to have this list at all. ListAdapter has methods to access the list you provide with submitList
Instead of
val articles_ = articles[position]
you can do
val articles_ = getItem(position)
and then you can remove the articles completely there so change
class MainAdapter(private val context: Context,private val articles:List<Article>)
to
class MainAdapter(private val context: Context)
and change
val mainAdapter = MainAdapter(this,article_)
to
val mainAdapter = MainAdapter(this)

Answer:
You are not implementing the getItemCount(). It returns the size of the list.
override fun getItemCount(): Int {
return articles.size
}
Cause of IndexOutOfBoundsException is that you are trying to access the index of list which is beyond the size of list.
Example:
The size of list is 2. So the elements will be at index 0 and 1. If you try to access index 2 then you will get to see IndexOutOfBoundsException

Related

My recyclerview resets when I add or delete an item from a Table (with Room) in Kotlin

First of all, I am Spanish so my english is not good.
I have an app with Kotlin and room, and it has a Recyclerview.
I have 3 tables: coaster, user and favorite.
The user can add coasters to favorite, and this is done succesfully.
The problem that I have is that when the user clicks on the button to add or delete from favorites, the recyclerview resets, it displays again. So it scrolls to the top of the Screen, and also some odd spaces appears after the element.
I also have a function to search, and it happens the same: spaces appears after each element when I am searching.
I have tried everything: notifyItemChanged,
notifyDataSetChanged... it doesnt work! I also tried removing the observer once from the recyclerview...
My main activity:
class CoasterFragment : Fragment() {
lateinit var coasterListener: CoasterListener
lateinit var usuarioCoaster: List\<UsuarioCoaster\>
private lateinit var searchView: SearchView
private lateinit var cAdapter: CoasterRecyclerViewAdapter
private var \_binding: FragmentCoasterBinding? = null
private val binding get() = \_binding!!
private val viewModel: CoastersViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCoasterBinding.inflate(inflater, container, false)
val root: View = binding.root
/* val livedata = viewModel.coasters()
livedata.observe(viewLifecycleOwner,object: Observer <List<CoasterFavorito>> {
override fun onChanged(it: List<CoasterFavorito>) {
createRecyclerView(it)
livedata.removeObserver(this)
}
})*/
viewModel.coasters().observe(viewLifecycleOwner){createRecyclerView(it)}
coasterListener = CoasterListenerImpl(requireContext(), viewModel)
searchView = binding.search
searchView.clearFocus()
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
})
return root
}
fun createRecyclerView(coasters: List<CoasterFavorito>) {
cAdapter =
CoasterRecyclerViewAdapter(
coasters as MutableList<CoasterFavorito>,
coasterListener,
requireContext()
)
val recyclerView = binding.recyclerCoaster
recyclerView.apply {
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
adapter = cAdapter
addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
cAdapter.notifyDataSetChanged()
}
}
fun searchDatabase(query: String) {
val searchQuery = "%$query%"
viewModel.searchDatabase(searchQuery).observe(viewLifecycleOwner) { createRecyclerView(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
my adapter:
class CoasterRecyclerViewAdapter(val coasters: List<CoasterFavorito>, val listener: CoasterListener,
val context: Context, ) : RecyclerView.Adapter<CoasterRecyclerViewAdapter.ViewHolder>(){
class ViewHolder private constructor(val binding: CoasterItemBinding, private val listener: CoasterListener,
private val context: Context): RecyclerView.ViewHolder(binding.root){
fun relleno(data: CoasterFavorito){
binding.nombre.text = data.coaster.nombre
binding.parque.text = data.coaster.parque
binding.ciudad.text = data.coaster.ciudad
binding.provincia.text = data.coaster.provincia
binding.comunidad.text = data.coaster.comunidadAutonoma
Glide
.with(context)
.load(data.coaster.imagen)
.centerCrop()
.into(binding.imagen)
binding.check.isChecked = data.favorito
binding.check.setOnClickListener{
if (data.favorito) {
listener.delFavorito(data.coaster.id)
binding.check.isChecked = false
} else {
listener.addFavorito(data.coaster.id)
binding.check.isChecked = true
}
}
}
companion object{
fun crearViewHolder(parent: ViewGroup, listener: CoasterListener, adapter: CoasterRecyclerViewAdapter, context: Context):ViewHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CoasterItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding, listener, context )
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder.crearViewHolder(parent, listener, this, context)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.relleno(coasters[position])
override fun getItemCount() = coasters.size
}
interface CoasterListener {
fun addFavorito(id: Long)
fun delFavorito(id: Long)
}
I have tried everything: notifyItemChanged,
notifyDataSetChanged... it doesnt work! I also tried removing the observer once from the recyclerview...
Your createRecyclerView function should be invoked only once in a whole lifecycle of the Fragment. You should not create any new RecyclerView.Adapter, or set a LayoutManager to the RecyclerView every time your data set changes.
Therefore the Observer used in viewModel.coasters.observe() should only submit a new List to the existing Adapter and call .notifyDataSetChanged(), or other notifying functions.

How to clear RecyclerView when updating data?

I have a RecyclerView with a ListAdapter. The list that is shown in the RecyclerView comes from a Flow that is observed in the Fragment that the recyclerView is instantiated.
When the Fragment is created, the data are calculated too (in onViewCreated Method) .
In the first data-calculation the RecyclerView is empty, a progressBar is shown, then the data is calculated, the progressbar hides, and the RecyclerView is populated.
If I go again in this Fragment to re-calculate the data, the previous list is shown simultaneously with the progressbar, and then is updated.
I want every time that new data is calculated to NOT show the previous list (just like the first data-calculation), but can't find a way to do it.
I tried to clear() the currentList and notifyDataSetChanged() but it still happens.. Any ideas?
Here is the code:
Fagment:
#AndroidEntryPoint
class YourPlanFragment : Fragment(R.layout.fragment_your_plan) {
lateinit var navController: NavController
private lateinit var binding: FragmentYourPlanBinding
private val sharedViewModel: WorkoutPlansViewModel by activityViewModels()
private lateinit var appBarConfiguration: AppBarConfiguration
#Inject
lateinit var dataStore: UserPreferencesRepo
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
binding = FragmentYourPlanBinding.bind(view)
appBarConfiguration = AppBarConfiguration(navController.graph)
val toolbar = binding.yourPlanToolbar
toolbar.setupWithNavController(navController, appBarConfiguration)
val exerciseAdapter = DayListAdapter(DayListAdapter.OnClickListener {
navigateTo(sharedViewModel.weekIndex, it.dayNumber)
})
exerciseAdapter.currentList.clear()
exerciseAdapter.notifyDataSetChanged()
binding.recyclerViewYourPlan.apply {
adapter = exerciseAdapter
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
recycledViewPool.clear()
removeAllViews()
adapter?.notifyDataSetChanged()
}
if (!sharedViewModel.planGenerated) {
// Here the data is generated
sharedViewModel.onTriggerEvent(WorkoutPlansEvent.GetWorkoutPlanEvent)
} else if (sharedViewModel.planGenerated) {
sharedViewModel.onTriggerEvent(WorkoutPlansEvent.GetWeekEvent)
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
sharedViewModel.yourPlanState.collect { yourPlanState ->
when (yourPlanState.progressBarState) {
is ProgressBarState.Loading -> {
binding.progressBar.isVisible = true
}
is ProgressBarState.Idle -> {
binding.progressBar.isInvisible = true
}
}
exerciseAdapter.submitList(yourPlanState.planDays)
}
}
}
}
private fun navigateTo(
currentWeek: Int,
dayNumber: Int,
) {
val action =
YourPlanFragmentDirections.actionYourPlanFragmentToYourDayFragment(
currentWeek,
dayNumber
)
navController.navigate(action)
}
}
ListAdapter:
class DayListAdapter(private val onClickListener: OnClickListener) :
ListAdapter<Day, DayListAdapter.DayViewHolder>(ExerciseComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayViewHolder {
val binding = ListItemDayBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return DayViewHolder(binding)
}
override fun onBindViewHolder(holder: DayViewHolder, position: Int) {
val currentItem = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(currentItem)
}
if (currentItem != null) {
holder.bind(currentItem)
}
}
class DayViewHolder(private val binding: ListItemDayBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(day: Day) {
binding.apply {
dayNumber = day.dayNumber.toString()
executePendingBindings()
}
}
}
class ExerciseComparator : DiffUtil.ItemCallback<Day>() {
override fun areItemsTheSame(oldItem: Day, newItem: Day) =
oldItem.dayNumber == newItem.dayNumber
override fun areContentsTheSame(oldItem: Day, newItem: Day) =
oldItem == newItem
}
class OnClickListener(val clickListener: (day: Day) -> Unit) {
fun onClick(day: Day) = clickListener(day)
}
}
This is happing because of the activityViewModels() that will preserve the viewmodel based on activity lifecycle. Either create WorkoutPlansViewModel using viewmodels() or Try to clear the list from the WorkoutPlansViewModel when you are creating the fragment.

ListAdapter with DiffUtil and LiveData overwriting items not appending

I implemented a ListAdapter with DiffUtil and faced an issue when appending a new list. It overwrites instead of appending to old one. To solve issue i created a new project and populate it with some test data.
Here is my code:
MainActivity
private lateinit var binding: ActivityMainBinding
private val viewModel: ItemViewModel by lazy {
ItemViewModel()
}
private val adapter: ItemAdapter by lazy {
ItemAdapter()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.getItems()
viewModel.items.observe(this, Observer { items ->
adapter.submitList(items)
})
binding.recyclerView.adapter = adapter
binding.fab.setOnClickListener {
viewModel.getItems(9)
}
}
ItemViewModel
class ItemViewModel: ViewModel() {
private val repository = FakeRepository()
private val _items: MutableLiveData<List<Item>> = MutableLiveData()
val items: LiveData<List<Item>> = _items
fun getItems(start: Int = 1) {
viewModelScope.launch {
val items = repository.getItems(start)
_items.value = items
/*val newItems = items.map { it.copy() }
_items.postValue(newItems)*/
}
}
}
ItemAdapter
class ItemAdapter: ListAdapter<Item, ItemAdapter.ViewHolder>(DiffUtilCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemRowBinding.inflate(LayoutInflater.from(parent.context),parent,false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ViewHolder(private val binding: ItemRowBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
binding.apply {
title.text = item.title
}
}
}
private class DiffUtilCallback: DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Item, newItem: Item) = oldItem == newItem
}
}
Item
data class Item(
val id: Int,
val title: String,
val timestamp: String
)
As per documentation:
Submits a new list to be diffed, and displayed.
If a list is already being displayed, a diff will be computed on a
background thread, which will dispatch Adapter.notifyItem events on
the main thread.
So, when you submit a new list via the LiveData observer, it's a brand new list to the adapter, and therefore it overwrites the current items not appending them.
If you want to append the current items, you can create a method in the adapter to consolidate the current list with the new one, and eventually submit it:
class ItemAdapter : ListAdapter<Item, ItemAdapter.ViewHolder>(DiffUtilCallback()) {
//......
fun appendList(list: List<Item>) {
val currentList = currentList.toMutableList() // get the current adapter list as a mutated list
currentList.addAll(list)
submitList(currentList)
}
}
And apply that to the observer callback in the activity:
viewModel.items.observe(this, Observer { items ->
// myAdapter.submitList(items) // send a brand new list
myAdapter.appendList(items) // Update the current list
})

Filling in a MultiView ViewHolder inside a RecyclerView With Data Binding

So I was able to get to a close point in creating the MultiView ViewHolder, but I am still a bit confused with some details. First how would I fill in the RecyclerView since I have multiple data classes(in this case, manually). Second, how would the Adapter know when to show a particular view? I'll leave the code here
Data Class(es)
sealed class InfoRecyclerViewItems{
class WithPicture (
val id: Int,
val movieName: String,
val thoughts: String
): InfoRecyclerViewItems()
class WithoutPicture(
val id: Int,
val movieName: String,
val thoughts: String
): InfoRecyclerViewItems()
}
The Adapter
class RecyclerViewAdapter(infoItems: MutableList<InfoRecyclerViewItems>): RecyclerView.Adapter<MainViewHolder>() {
private var infoItems1: MutableList<InfoRecyclerViewItems>
init {
this.infoItems1 = infoItems
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
return when(viewType){
R.layout.container_one -> MainViewHolder.WithPictureViewHolder(
ContainerOneBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
)
R.layout.container_two -> MainViewHolder.WithoutPictureViewHolder(
ContainerTwoBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
)
else -> throw IllegalArgumentException("Invalid view given")
}
}
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
when(holder){
is MainViewHolder.WithPictureViewHolder -> holder.bind(infoItems1[position] as InfoRecyclerViewItems.WithPicture)
is MainViewHolder.WithoutPictureViewHolder -> holder.bind(infoItems1[position] as InfoRecyclerViewItems.WithoutPicture)
}
}
override fun getItemCount() = infoItems1.size
override fun getItemViewType(position: Int): Int {
return when(infoItems1[position]){
is InfoRecyclerViewItems.WithPicture -> R.layout.container_one
is InfoRecyclerViewItems.WithoutPicture -> R.layout.container_two
}
}
}
The ViewHolder(s)
sealed class MainViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
class WithPictureViewHolder(private val binding: ContainerOneBinding) : MainViewHolder(binding){
fun bind(items: InfoRecyclerViewItems.WithPicture){
binding.part1 = items
binding.executePendingBindings()
}
}
class WithoutPictureViewHolder(private val binding: ContainerTwoBinding) : MainViewHolder(binding){
fun bind(items: InfoRecyclerViewItems.WithoutPicture){
binding.part2 = items
binding.executePendingBindings()
}
}
}
Main Activity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.recyclerView.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(this#MainActivity)
}
}
}
Any suggestions are welcomed, Thank You.
SO I figured it out. based on this setup, I can create an empty MutableList and then call each different data class that I want to fill in. those data classes are linked to the ViewHolder that it is associated to, thus creating two different views inside the RecylerView.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var manager: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val information: MutableList<InfoRecyclerViewItems> = ArrayList()
information.add(InfoRecyclerViewItems.WithPicture(2, "The Fallen", "ok"))
information.add(InfoRecyclerViewItems.WithoutPicture(4, "Black Panther", "10/10, Fantastic Movie"))
manager = LinearLayoutManager(this)
binding.recyclerView.apply {
adapter = RecyclerViewAdapter(information)
layoutManager = manager
}
}
}
Then you can just keep adding to whatever view you chose to add it to
P.S. The other files(ViewHolder, Adapter and Data Class) stay the same.

Delete Item in RecyclerView and Dao with buttonclick

I have a RecyclerView displaying data from a Dao.
In the data that is shown in the recyclerView is a button with which the item should get deleted.
I know the function for that is :
fun deleteReceipt(receipts: Receipts) = viewModelScope.launch {
receiptDao.delete(receipts)
}
But I am not quite sure where to put it and call it. Because if I want to call it in the Adapter where i set the other displayed Item values I have no access to the Dao
Here is the fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
setHasFixedSize(true)
}
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){ /// New Items get passed to the List
exampleAdapter.submitList(it)
}
}
}
The ViewModel:
#HiltViewModel
class PurchaseViewmodel #Inject constructor(
private val receiptDao: ReceiptDao
): ViewModel() {
private val tasksEventChannel = Channel<TasksEvent>()
val addTaskEvent = tasksEventChannel.receiveAsFlow()
val receipts = receiptDao.getAllReceipts().asLiveData()
fun onAddResult(result: Int){
when (result){
ADD_RECEIPT_RESULT_OK ->showReceiptSavedConfirmation("Receipt is saved")
}
}
private fun showReceiptSavedConfirmation (text: String) = viewModelScope.launch {
tasksEventChannel.send(TasksEvent.ShowReceiptSavedConfirmation(text))
}
fun deleteReceipt(receipts: Receipts) = viewModelScope.launch {
receiptDao.delete(receipts)
}
sealed class TasksEvent {
data class ShowReceiptSavedConfirmation(val msg: String) : TasksEvent()
}
}
And the Adapter:
class ExampleAdapter : ListAdapter<Receipts,ExampleAdapter.ExampleViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder { // Basically how to get a new Item from the List and display it
val binding = ReceiptsBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return ExampleViewHolder(binding)
}
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
class ExampleViewHolder(private val binding: ReceiptsBinding) : RecyclerView.ViewHolder(binding.root){ //Examples One Row in our list
fun bind (receipts: Receipts) {
binding.apply {
storeHistory.text = receipts.store
amountHistory.text = receipts.total
dateHistory.text = receipts.date
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Receipts>() {
override fun areItemsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem == newItem
}
}
Create a listener using Interface
Implement the listener in Fragment
Create listener variable in Adapter
pass the listener from fragment to adapter
pass the listener to viewHolder
on Delete button click of that item call the listener method, which will trigger the implementation in Fragment where you have access to your ViewModel
delete the receipt using viewModel.deleteReceipt

Categories

Resources