I need to setup ViewPager2 in a way where I dynamically update the Fragments that are displayed within the ViewPager2.
To facilitate this, I've created a view model LiveData object that includes a list of items that represent the data the fragments should display:
val items: LiveData<List<Item>>
In my Fragment that contains the ViewPager2, in onViewCreated I setup observing the items:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.items.observe(viewLifecycleOwner) { items ->
binding.viewPager.adapter = createFragmentStateAdapter(items)
}
}
This is all working fine except when I test with "Don't keep activities" turned on. When I background/foreground the app I see that the Fragment containing the ViewPager2 is recreated and that the items observable is broadcasting an update. However, for some reason the ViewPager2 is showing an older Fragment and appears to be ignoring the new adapter that is created in the items observe block.
How do I get the ViewPager2 to update correctly when the adapter changes?
I believe what might be happening is that the ViewPager2 is trying to restore its own state but in the process is ignoring recent changes to the adapter to use.
Adapter creation should not be placed in observe, because it will create an Adapter every time the data is updated. The adapter should be created in viewCreated, and there are operations to add data and update the UI inside the Adapter.
class DemoAdapter: RecyclerView.Adapter<DemoAdapter.VHolder>(){
class VHolder(view: View): RecyclerView.ViewHolder(view)
private var dataList: List<String> = emptyList()
fun setDataList(dataList: List<String>) {
this.dataList = dataList
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VHolder {
// create ViewHolder
return VHolder(View(parent.context))
}
override fun onBindViewHolder(holder: VHolder, position: Int) {
// bind data
}
override fun getItemCount(): Int {
}
}
val adapter = DemoAdapter()
binding.viewPager.adapter = adapter
viewModel.items.observe(viewLifecycleOwner) { items ->
adapter.setDataList(items)
}
I found a solution by overriding getItemId and containsItem in the FragmentStateAdapter I was using
Related
I'm starting using Kotlin (i'm a web dev) to maintain the mobile app of my current job. To practice my learning, I'm creating a basic app which is displaying a list of France departments (using a REST Api), and I need to allow the user to click on a list item to get more info on the selected item.
I'm trying to build this with databinding, Koin as dependency injection, and Room as db layer.
My issue is that I created a RecyclerView custom Adapter, and used the databinding to give it the datas. But now I want to implement the onClick behaviour, which should launch another activity to display item details. My problem is: I don't know how to do this in a clean way.
I was thinking about creating a viewModel to link to my Adapter, but can't really find how to do it well. And even if I did, how to start another activity in a viewModel ? (don't have access to the context and startActivity function). So I finally dropped that solution that doesn't seems to fit.
So I'm currently thinking of passing directly from my adapter the onClick function, but can't find a way to bind this function in my xml file. Here is my files:
MainActivity:
class MainActivity : AppCompatActivity() {
private val mViewModel: DepartmentsViewModel by viewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.title = "Liste des départements"
val adapter = DepartmentListAdaptater()
binding.recyclerview.adapter = adapter
binding.recyclerview.layoutManager = LinearLayoutManager(this)
mViewModel.allDepartments.observe(this, Observer { data -> adapter.submitList(data) })
}
}
RecyclerView.Adapter:
class DepartmentListAdaptater : RecyclerView.Adapter<DepartmentListAdaptater.ViewHolder>() {
private var dataSet: List<Department>? = null
inner class ViewHolder(private val binding: DepartmentListRowBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(department: Department?) {
binding.department = department
}
}
fun submitList(list: List<Department>) {
dataSet = list
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = DepartmentListRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun getItemCount(): Int = dataSet?.size ?: 0
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(dataSet?.get(position))
}
}
The XML View:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="department" type="com.navalex.francemap.data.entity.Department" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="72dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="#drawable/list_item_bg"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:clickable="true"
tools:ignore="UselessParent">
<TextView
android:id="#+id/textView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingRight="16dp"
android:text="#{department.nom}"
android:paddingLeft="16dp"/>
</LinearLayout>
</RelativeLayout>
</layout>
First I want to say that it's really impressive that you are a web developer and you already have a lot of knowledge about things like dependency injection and keep the state of the view on ViewModel, congrats. Now, let's talk about your problem... I'll start with some suggestions that will improve the code clarity and performance.
For the Adapter implementation, always prefer to use ListAdapter, because this implementation have a more efficient way to compare the current list with the new list and update it. You can follow this example:
class MyAdapter: ListAdapter<ItemModel, MyAdapter.MyViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = FragmentFirstBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
class MyViewHolder(
private val binding: FragmentFirstBinding
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: ItemModel) {
// Here you can get the item values to put this values on your view
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ItemModel>() {
override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// need a unique identifier to have sure they are the same item. could be a comparison of ids. In this case, that is just a list of strings just compare like this below
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// compare the objects
return oldItem == newItem
}
}
}
}
In your fragment, you have a observer, that observe the value you want to sent to the adapter, right? When a update happen, you call the submitList sending the updated list and when the adapter receive this new list, the adapter will be responsible to update just the items that changed, because of your DIFF_CALLBACK implementation.
About the onClick item, you can wait for a callback on your adapter. Doing this:
class MyAdapter(
private val onItemClicked: (item: ItemModel) -> Unit
): ListAdapter<ItemModel, MyAdapter.MyViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = FragmentFirstBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding, onItemClicked)
}
// ...
class MyViewHolder(
private val binding: FragmentFirstBinding,
private val onItemClicked: (item: ItemModel) -> Unit
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: ItemModel) {
// ...
// Here you set the callback to a listener
binding.root.setOnClickListener {
onItemClicked.invoke(item)
}
}
}
// ...
}
As you can see, we will receive the callback on the Adapter constructor, then we send to the ViewHolder by constructor too. And on the ViewHolder bind we set the callback to a click listener.
On you fragment, you will have something like this:
class MyFragment: Fragment() {
private lateinit var adapter: MyAdapter
private val onItemClicked: (itemModel: ItemModel) -> Unit = { itemModel ->
// do something here when the item is clicked, like redirect to another activity
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = MyAdapter(onItemClicked)
}
}
I hope it helps you. Please, let me know if you need something more. I really appreciate helping.
I don't know about data binding specifically, but a typical way to do it is to let the Activity handle details like app navigation, and let the Adapter trigger that logic. A listener function is an easy way to do this:
// in your Adapter
var clickListener: ((YourData) -> ())? = null
// in your ViewHolder (make it an inner class so it can access the Adapter's
// fields, like the listener object and the stored data)
init {
clickableView.setOnClickListener {
// pass back whatever data here, if the listener needs to know
// what's been clicked. I'm just doing a lookup and passing
// the data item currently being displayed
clickListener?.invoke(
adapterData[bindingAdapterPosition]
)
}
}
// in your Activity, when setting up the adapter
adapter.clickListener = { whateverData ->
// do what you need to do in response to the click
}
So the Activity itself is defining that logic about actions that should be taken when a click happens - it's basically wiring up different parts of the app, so the Adapter doesn't need to be concerned with anything except taking data, displaying it, and informing a listener when specific interactions take place. That listener code (defined by the Activity) could navigate somewhere else, or update a database, or pass it to a networking component... the adapter doesn't need to know about that.
(The non-Kotlin way to do this would be to create an interface and have the Activity implement that, and pass itself as the listener/callback object, that kind of thing)
Faced such a problem: I have a RecyclerView, the data for which I get from the ViewModel using StateFlow:
viewModel.items.collect {
setRecyclerView(items)
}
Then, let's say, somewhere inside the Fragment, I change the data for items and there are more of them. In order for my RecyclerView to see my changes, I have to call the setRecyclerView(items) function again, which, it seems to me, can lead to the most unexpected consequences (for example, items will be duplicated). The question is: is it possible to somehow change the data and update the RecyclerView (including the onBindViewHolder function in it) without yet another creation of an Adapter?
Let's start talking about the adapter implementation. Reading your question, I believe you used RecyclerView.Adapter to implement your adapter. But there is another option that is simpler and more performant than this. It's the ListAdapter:
The most interesting thing about ListAdapter is the DiffUtil, that have a performative way to check if any item on your list was updated, deleted, or included. Here's a sample of the implementation:
abstract class MyAdapter: ListAdapter<ItemModel, MyAdapter.MyViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = ItemSimplePosterBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
class MyViewHolder(
private val binding: ItemSimplePosterBinding
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: ItemModel) {
// Here you can get the item values to put these values on your view
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ItemModel>() {
override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// need a unique identifier to have sure they are the same item. could be a comparison of ids. In this case, that is just a list of strings just compares like this below
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// compare the objects
return oldItem == newItem
}
}
}
}
So, when your list is updated, you just have to call the submitList from the adapter, like this:
viewModel.items.collectLatest { items ->
// You will send the items to your adapter here
adapter.submitList(items)
}
Then, your RecyclerView just has to be configured on onViewCreated for example, and your list can be defined and updated in another place, observing the items change from ViewModel.
I am using nested recyclerview.
In the picture, the red box is the Routine Item (Parent Item), and the blue box is the Detail Item (Child Item) in the Routine Item.
You can add a parent item dynamically by clicking the ADD ROUTINE button.
Similarly, child items can be added dynamically by clicking the ADD button of the parent item.
As a result, this function works just fine.
But the problem is in the code I wrote.
I use a ViewModel to observe and update parent item addition/deletion.
However, it does not observe changes in the detail item within the parent item.
I think it's because LiveData only detects additions and deletions to the List.
So I put _items.value = _items.value code to make it observable when child items are added and deleted.
This way, I didn't even have to use update code like notifyDataSetChanged() in the child adapter.
In the end it is a success, but I don't know if this is the correct code.
Let me know if you have additional code you want!
In Fragment.kt
class WriteRoutineFragment : Fragment() {
private var _binding : FragmentWriteRoutineBinding? = null
private val binding get() = _binding!!
private lateinit var adapter : RoutineAdapter
private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)
adapter = RoutineAdapter(::addDetail, ::deleteDetail)
binding.rv.adapter = this.adapter
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getTabPageResult()
// RecyclerView Update
vm.items.observe(viewLifecycleOwner) { updatedItems ->
adapter.setItems(updatedItems)
}
}
private fun getTabPageResult() {
val navController = findNavController()
navController.currentBackStackEntry?.also { stack ->
stack.savedStateHandle.getLiveData<String>("workout")?.observe(
viewLifecycleOwner, Observer { result ->
vm.addRoutine(result) // ADD ROUTINE
stack.savedStateHandle?.remove<String>("workout")
}
)
}
}
private fun addDetail(pos: Int) {
vm.addDetail(pos)
}
private fun deleteDetail(pos: Int) {
vm.deleteDetail(pos)
}
}
ViewModel
class WriteRoutineViewModel : ViewModel() {
private var _items: MutableLiveData<ArrayList<RoutineModel>> = MutableLiveData(arrayListOf())
val items: LiveData<ArrayList<RoutineModel>> = _items
fun addRoutine(workout: String) {
val item = RoutineModel(workout, "TEST")
_items.value?.add(item)
// _items.value = _items.value
}
fun addDetail(pos: Int) {
val detail = RoutineDetailModel("TEST", "TEST")
_items.value?.get(pos)?.addSubItem(detail) // Changing the parent item's details cannot be observed by LiveData.
_items.value = _items.value // is this right way?
}
fun deleteDetail(pos: Int) {
if(_items.value?.get(pos)?.getSubItemSize()!! > 1)
_items.value?.get(pos)?.deleteSubItem() // is this right way?
else
_items.value?.removeAt(pos)
_items.value = _items.value // is this right way?
}
}
This is pretty standard practice when using a LiveData with a mutable List type. The code looks like a smell, but it is so common that I think it's acceptable and people who understand LiveData will understand what your code is doing.
However, I much prefer using read-only Lists and immutable model objects if they will be used with RecyclerViews. It's less error prone, and it's necessary if you want to use ListAdapter, which is much better for performance than a regular Adapter. Your current code reloads the entire list into the RecyclerView every time there is any change, which can make your UI feel laggy. ListAdapter analyzes automatically on a background thread your List for which items specifically changed and only rebinds the changed items. But it requires a brand new List instance each time there is a change, so it makes sense to only use read-only Lists if you want to support using it.
I'm just beginning to learn kotlin and my app is vey basic but this basic functionality has been giving me trouble the past couple of days.
I have one activity with two fragments (A and B) in my app. Fragment A is used to display the data in recyclerview. Fragment B is used to add data using edit texts/add button etc. That all works fine. Then when user selects an item in the recyclerview in Fragment A I want to navigate back to Fragment B passing an argument to say the user is now editing now adding and populate the edit texts with the selected items fields so the user can edit.
My Adapter:
class MoviesAdapter constructor(private var movies: List<MovieModel>)
: RecyclerView.Adapter<MoviesAdapter.MainHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainHolder {
return MainHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.card_movie,
parent,
false
)
)
}
override fun onBindViewHolder(holder: MainHolder, position: Int) {
val movie = movies[holder.adapterPosition]
holder.bind(movie)
}
override fun getItemCount(): Int = movies.size
class MainHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(movie: MovieModel) {
itemView.movie_view_Title.text = movie.title
itemView.movie_view_director.text = movie.director
itemView.movie_view_releaseDate.text = movie.releaseDate.toString()
itemView.movie_view_ratingBar.rating = movie.rating.toFloat()
itemView.movie_view_Image.setImageBitmap(readImageFromPath(itemView.context,movie.image))
itemView.setOnClickListener{
//Pass the movie item to Fragment B to edit & pass argument to say we are editing not adding
}
}
}
}
add context in constructor
class MoviesAdapter constructor (mCTX:Context,private var movies: List<MovieModel>){
.....
}
start Second Activity or Fragment using context in Adapter
Pass data using Android Bundle()
or
Implement a Interface
I want to display new list of items retrieved from ViewModel in custom RecyclerView.Adapter. To do so I pass the retrieved list to adapter and invoke notifyDataSetChanged(), but nothing changes on the UI.
I've debugged code and adapter's list had 1 element (UI displayed 1 element), new retrieved list had 2 elements - after setting new list in adapter and invoking notifyDataSetChanged() UI didn't change, but should append that 1 element.
Fragment's code:
trip_details_participantsList.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
trip_details_participantsList.adapter = userBriefAdapter
tripsDetailsViewModel.getTripParticipants(tripId).observe(this, Observer {participants ->
tripsDetailsViewModel.getUsersBriefs(participants.map { x -> x.userId }).observe(this, Observer{ users ->
userBriefAdapter.setData(users)
})
})
Custom Adapter and ViewHolder:
class UserBriefAdapter : RecyclerView.Adapter<UserBriefViewHolder>() {
private var users: List<UserBrief> = mutableListOf()
fun setData(items: List<UserBrief>){
this.users = items
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: UserBriefViewHolder, position: Int) =
holder.bind(users[position])
override fun getItemCount() = users.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserBriefViewHolder {
val inflater = LayoutInflater.from(parent.context)
return UserBriefViewHolder(inflater, parent)
}
}
class UserBriefViewHolder(inflater: LayoutInflater, parent: ViewGroup) : RecyclerView.ViewHolder(inflater.inflate(
R.layout.trip_details_list_item, parent, false)) {
private var profilePictureImageView: ImageView = itemView.findViewById(R.id.trips_details_profile_image)
fun bind(userBrief: UserBrief){
Picasso.get()
.load(userBrief.profilePictureUrl)
.into(profilePictureImageView)
}
}
I've read existing topics about it, but every accepted solution is something like my adapter's setData() function with notifyDataSetChanged(). I've also tried to set new instance of UserBriefAdapter to RecyclerView each time observed values changed, but results were the same. When I go back and forth the view, then it correctly displays the elements, but I want to achieve this without changing the view.
It could be an issue of threading. Try to post a runnable to the main thread in your observer callback:
tripsDetailsViewModel.getUsersBriefs(participants.map { x -> x.userId }).observe(this, Observer{ users ->
view?.post { userBriefAdapter.setData(users) }
})
Done some more debugging and figured out that notifyDataSetChanged() wasn't the problem. I was passing wrong profilePictureUrl to UserBriefViewHolder, so Picasso library doesn't fetch an image and in consequence UI wasn't updated.