I am trying to have a nested RecyclerView where a Horizontal RecyclerView will be shown as an item of Vertical RecyclerView. (UI looks similar to Google Play Store)
Since my dataset is in FirebaseFirestore, I am using FirestoreRecyclerAdapter to achieve this.
My Fragment's code (Parent RecyclerView exists here):
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.layout_recyclerview, container, false)
val query = <some reference>
val recycler = view.recyclerView
recycler.setHasFixedSize(true)
adapter = DashboardAdapter(this,
FirestoreRecyclerOptions.Builder<Category>().categoryOption(query, this),
R.layout.item_dashboard_row)
recycler.adapter = adapter
return view
}
DashboardAdapter snippet:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DashboardHolder {
val item = LayoutInflater.from(parent.context)
.inflate(layout, parent, false)
return DashboardHolder(item)
}
DashboardHolder snippet:
internal class DashboardHolder(item: View) : RecyclerView.ViewHolder(item) {
private val rowTitle: TextView = item.rowTitle
private val rowRecycler: RecyclerView = item.rowRecycler
fun bind(category: Category, owner: LifecycleOwner) {
rowTitle.text = category.name
rowRecycler.setHasFixedSize(true)
val query = <some query>
val adapter = DashboardProductsAdapter(
FirestoreRecyclerOptions.Builder<Product>()
.productOption(query, owner),
R.layout.item_dashboard_product)
rowRecycler.adapter = adapter
}
}
It's clear that DashboardHolder(the parent view holder) has RecyclerView in it. And while binding, the child adapter is created and set to the child RecyclerView.
When I am loading the Fragment for the first time, everything works fine and loads properly. But after I click Home button and come back to the app again, only the parent RecyclerView is getting populated, not the child ones.
After I started digging more, figured out that it's because of LifecycleOwner I am passing while creating FirestoreRecyclerOptions. If I don't set it and manually call startListening() and stopListening(), then also the behavior is same. But if I don't call stopListening(), it works fine.
Updated Fragment's code:
override fun onStart() {
super.onStart()
adapter.startListening()
}
override fun onStop() {
super.onStop()
// If I comment this out, everything works fine
// But putting this in code doesn't populate the child RecyclerView 2nd time
adapter.stopListening()
}
What could be the possible problem? Shall I create the child adapter outside the bind() method? Shall I skip stopListening() callback, but this might lead to memory leak.
I think there are several things that could be going wrong here. I'll update this answer once we figure it out.
First, I want to make sure you're calling bind() in onBindViewHolder(), right?
Next, I'm pretty sure this doesn't matter, but could you move your init code to onViewCreated() like so:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.layout_recyclerview, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val query = <some reference>
val recycler = view.recyclerView
recycler.setHasFixedSize(true)
adapter = DashboardAdapter(this,
FirestoreRecyclerOptions.Builder<Category>().categoryOption(query, this),
R.layout.item_dashboard_row)
recycler.adapter = adapter
}
As for your view holders, there's one core problem you'll need to solve which is a little tricky to think about. bind() is going to be called numerous times between onStart() and onStop which means you're going to be left with a large number of hanging adapters because stopListening() won't be called when an adapter is rebound. To get around this, you'll need to save your adapter in a property and clear it every time in bind() like so:
private var currentAdapter: FirestoreRecyclerAdapter<...>? = null
fun bind(...) {
currentAdapter?.let {
it.stopListening() // Stop listening to database
lifecycle.removeObserver(it) // Prevent automatic readdition of listeners
}
// Init adapter
currentAdapter = ...
}
If none of the above works, you'll need to do some hardcore debugging by stepping through your code bit by bit until you see where things are falling apart. These are the breakpoints I would suggest adding:
The adapter's startListening() method, ensure addChangeEventListener() is called
The adapter's onChildChanged() method, ensure it's being called with correct values
Walk the stack to look through your memory and ensure the references you're holding are the current objects and not ghosts from onStop()
Everything is being called in the right place? Probably not a FirebaseUI or Architecture Components issue. BTW, ensure you're on the latest FUI version 3.1.0 and AAC version rc1.
Related
Do I have to free my custom ArrayAdapters in fragments, like I do it with binding? And also what about ArrayList that holds the data?
Inside my Fragment class I have:
private var binding: FragmentUhfReadBinding? = null // init onCreateView, free onDestroyView, use onViewCreated
private var listAdapter: ReadUhfTagInfoItemViewAdapter? = null // init in onViewCreated
private val arrayList: ArrayList<ReadUhfTagInfo?>? = ArrayList()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentUhfReadBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
// do I have to set `listAdapter` to null here?
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listAdapter = ReadUhfTagInfoItemViewAdapter(this.context, arrayList)
binding!!.lvData.adapter = listAdapter
}
Yes, because your adapter is referencing a context (which is the attached Activity).
It's also referencing a list that you have created only in your Fragment, but it's debatable whether you should care if that's leaked. It would be more typical for backing list data to be in a ViewModel so it could be reused, in which case this list reference your adapter is holding wouldn't be a problem.
Although your example doesn't show it, it's also very common for an adapter to expose some kind of listener for view interactions. So when you add this, and your listeners capture references to other stuff in the fragment, then the adapter would also be leaking the memory of those things.
However, it is not common in the first place to need your binding and your adapter to be in class properties. If you use them solely within onViewCreated() and functions called by onViewCreated(), then you don't need to worry about clearing references to them, and you won't have the ugly use of !!, so it will be more robust.
Instead of inflating a view with onCreateView(), you can pass a layout id to the superconstructor, and then create your binding by calling bind with the pre-existing view in onCreateView().
class MyFragment: Fragment(R.layout.fragment_uhf_read) {
private val arrayList: ArrayList<ReadUhfTagInfo?> = ArrayList()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentUhfReadBinding.bind(view)
val listAdapter = ReadUhfTagInfoItemViewAdapter(this.context, arrayList)
binding.lvData.adapter = listAdapter
// ... other code using binding and listAdapter
}
}
As per the android documentation, To get the data binding within a fragment, I use a non-nullable getter, but sometimes' When I try to access it again, after I'm wait for the user to do something, I receive a NullPointerException.
private var _binding: ResultProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = ResultProfileBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun setupViews() {
// On click listener initialized right after view created, all is well so far.
binding.btnCheckResult.setOnClickListener {
// There is the crash when trying to get the nonnull binding.
binding.isLoading = true
}
}
Does anyone know what the cause of the NullPointerException crash is? I'm trying to avoid not working according to the android documentation, and do not return to use nullable binding property (e.g _binding?.isLoading). Is there another way?
I can't explain why you're having any issue in the code above since a View's click listener can only be called while it is on screen, which must logically be before onDestroyView() gets called. However, you also asked if there's any other way. Personally, I find that I never need to put the binding in a property in the first place, which would completely avoid the whole issue.
You can instead inflate the view normally, or using the constructor shortcut that I'm using in the example below that lets you skip overriding the onCreateView function. Then you can attach your binding to the existing view using bind() instead of inflate(), and then use it exclusively inside the onViewCreated() function. Granted, I have never used data binding, so I am just assuming there is a bind function like view binding has.
class MyFragment: Fragment(R.layout.result_profile) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = ResultProfileBinding.bind(view)
// Set up your UI here. Just avoid passing binding to callbacks that might
// hold the reference until after the Fragment view is destroyed, such
// as a network request callback, since that would leak the views.
// But it would be fine if passing it to a coroutine launched using
// viewLifecycleOwner.lifecycleScope.launch if it cooperates with
// cancellation.
}
}
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 have a fragment that display a recyclerview widget
when the list is empty, i want to display an imagebutton in the middle to tell the user to add content.
I link the view in onCreateView this way
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view : View
val repository = CrimeRepository.get()
val crimes : LiveData<List<Crime>> = repository.getCrimes()
if (crimes.value?.isEmpty() == true){
view= inflater.inflate(R.layout.fragment_crime_list, container, false)
crimeRecyclerView =
view?.findViewById(R.id.crime_recycler_view) as RecyclerView
crimeRecyclerView?.layoutManager= LinearLayoutManager(context)
} else{
view = inflater.inflate(R.layout.empty_layout, container, false)
imageButton = view.findViewById(R.id.imageButton) as ImageButton
imageButton.setOnClickListener{
val crime = Crime()
crimeListViewModel.addCrime(crime)
callbacks?.onCrimeSelected(crime.id)
}
}
return view
}
my problem is getting the size of the crimes list... i only can get .value and that is never null even if it is empty...
usually i get it inside onViewCreated this way
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
crimeListViewModel.crimeListLiveData.observe(
viewLifecycleOwner,
Observer { crimes ->
crimes?.let {
Log.i(TAG, "Got Crimes ${crimes.size}")
updateUI(crimes)
}
}
)
}
I tried to do the same inside onCreateView, that never worked... maybe i can't find the right way to make the code...
when i get the data i can check if the size is zero or not...
any solution?
i only can get .value and that is never null even if it is empty...
This is because the operation is asynchronous which means you have to wait before accessing the real value of crime list. You can do this by using observe which you already use but not in the right way.
Here is a nice and clean way to achieve what you want.
Use the xml layout you have for both the recyclerView and the imageButton. (Make sure to use FrameLayout as root layout)
Use android:visibillity=gone to imageButton by default
Inflate the layout as you already do in onCreateView but remove the inflation of imageButton
Observe live data in OnViewCreated
As soon as you get some data then act on it. You can have something like
crimeListViewModel.crimeListLiveData.observe(viewLifecycleOwner) { crimes ->
if(crimes.isNullOrEmpty() {
// show imageButton
// add click listener to imageButton
} else {
// hide imageButton
// populate the recyclerView adapter with Items
} }
)
Here is my code:
class HomeFragment : Fragment() {
val viewModel by lazy {
ViewModelProvider(this)[HomeViewModel::class.java]
}
private lateinit var adapter: ArticleListAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_home, container, false)
return root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val linearLayoutManager = LinearLayoutManager(activity)
rv_home.layoutManager = linearLayoutManager
adapter = ArticleListAdapter(viewModel.articleList)
rv_home.adapter = adapter
viewModel.getHomeArticle(0)
viewModel.articleLiveData.observe(viewLifecycleOwner, { result ->
val HomePages = result.getOrNull()
viewModel.articleList = HomePages
adapter.notifyDataSetChanged()
})
}
}
class HomeViewModel : ViewModel() {
var articleList: HomeArticleBean? = null
private val observerArticleLiveData = MutableLiveData<Int>()
val articleLiveData = Transformations.switchMap(observerArticleLiveData) { page ->
Repository.getHomeArticles(page)
}
fun getHomeArticle(page: Int) {
observerArticleLiveData.value = page
}
}
When I call getHomeArticle() from HomeFragment,the program should execute
Repository.getHomeArticles(page)
But after Dubug I found this line of code would not be executed.
This caused my articleLiveData observation in HomeFragment to be invalid, thus preventing me from updating the UI. I'm new to Jetpack so I don't know why this is happening, my code was written after someone else's code and I had no problems running his code, I looked carefully to see if there were differences between my code and his but didn't find any. I am searching for a long time on the net. But no use. Please help or try to give some ideas on how to achieve this. Thanks in advance.
Your problem is with your use of viewModel.articleList:
adapter = ArticleListAdapter(viewModel.articleList)
This creates your adapter and ties its data to the list currently stored at viewModel.articleList (which, for some reason, isn't in your HomeViewModel code?)
viewModel.articleList = HomePages
This line completely throws away the list currently stored at viewModel.articleList and re-assigns the variable to store a completely new list. Your adapter knows nothing about this new list - it still has a reference to the old list.
Therefore you have a couple of choices:
Have your ArticleListAdapter have a setData method that takes the new list. That setData would update the list the ArcticleListAdapter is keeping track of (and, as a benefit, it could also internally call notifyDataSetChanged(), making it is always called.
Update the viewModel.articleList with new entries, rather than replace the whole list, thus ensuring that the list that ArticleListAdapter is using has your updated data:
viewModel.articleLiveData.observe(viewLifecycleOwner, { result ->
val HomePages = result.getOrNull()
viewModel.articleList.clear()
if (HomePages != null) {
viewModel.articleList.addAll(HomePages)
}
adapter.notifyDataSetChanged()
})
Make your ArticleListAdapter extend ListAdapter. This class handles tracking the list of data for you and gives you a simple submitList method which you'd call from your observe method. As a side benefit, it uses DiffUtil which is way more efficient than notifyDataSetChanged().