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().
Related
I'm a rookie Android developer, and could use a little guidance regarding traversing a LiveData List in the ViewModel.
I am basing my app on the MVVM design, and it is simply scanning folders for images, and adding some folders to a favourites list I store in a database. During the scans, I need to check with the stored favourites to see if any of the scanned folders are favourites.
It is the "check against the stored favourites" part that gives me trouble.
Here are the relevant bits from my fragment:
class FoldersFragment : Fragment(), KodeinAware {
override val kodein by kodein()
private val factory: FoldersViewModelFactory by instance()
private var _binding: FragmentFoldersBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: FoldersViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFoldersBinding.inflate(inflater, container, false)
val root: View = binding.root
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(FoldersViewModel::class.java)
binding.rvFolderList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = FolderItemAdapter(listOf(), viewModel)
binding.rvFolderList.adapter = adapter
viewModel.getFolderList().observe(viewLifecycleOwner, {
adapter.folderItems = it
binding.rvFolderList.adapter = adapter // Forces redrawing of the recyclerview
})
...
}
Now, that observer work just fine - it picks up changes and my RecyclerView responds with delight; all is well.
Here are the relevant bits from my RecyclerView adapter:
class FolderItemAdapter(var folderItems: List<FolderItem>, private val viewModel: FoldersViewModel):
RecyclerView.Adapter<FolderItemAdapter.FolderViewHolder>() {
private lateinit var binding: FolderItemBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
binding = FolderItemBinding.inflate(LayoutInflater.from(parent.context))
val view = binding.root
return FolderViewHolder(view)
}
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
val currentItem = folderItems[position]
...
if (viewModel.isFavourite(currentItem)) {
// do stuff
}
...
}
}
And with that, my problem; the check viewModel.isFavourite(currentItem)always returns false.
The implementation in my ViewModel is:
class FoldersViewModel(private val repository: FoldersRepository) : ViewModel() {
fun getImageFolders() = repository.getImageFolders()
fun isFavourite(item: FolderItem): Boolean {
var retval = false
getImageFolders().value?.forEach {
if (it.path == item.path) {
retval = true
}
}
}
}
The `getImageFolders() function is straight from the repository, which again is straight from the Dao:
#Dao
interface FoldersDao {
#Query("SELECT * FROM image_folders")
fun getImageFolders(): LiveData<List<FolderItem>>
}
My problem is that I simply can't traverse that list of favourites in the ViewModel. The isFavourite(item: FolderItem) function always returns false because getImageFolders().value always is null. When I check getImageFolders() it is androidx.room.RoomTrackingLiveData#d0d6d31.
And the conundrum; the observer is doing the exact same thing? Or isn't it?
I suspect I am not understanding something basic here?
Your getImageFolders() function retrieves something asynchronously from the database, because you specified that it returns a LiveData. When you get the LiveData back, it will not immediately have a value available. That's why your .value?.forEach is never called. value is still null because you're trying to read it immediately. A LiveData is meant to be observed to obtain the value when it arrives.
There are multiple ways to make a DAO function return something without blocking the current thread. (Handy table here.) Returning a LiveData is one way, but it's pretty awkward to use if you only want one value back. Instead, you should use something from the One-shot read row in the linked table.
If you aren't using RxJava or Guava libraries, that leaves a Kotlin coroutines suspend function as the natural choice.
That would make your Dao look like:
#Dao
interface FoldersDao {
#Query("SELECT * FROM image_folders")
suspend fun getImageFolders(): List<FolderItem>
}
And then your ViewModel function would look like:
suspend fun isFavourite(item: FolderItem): Boolean {
return getImageFolders().any { it.path == item.path }
}
Note that since it is a suspend function, it can only be called from a coroutine. This is necessary to avoid blocking the main thread. If you're not ready to learn coroutines yet, you can replace this function with a callback type function like this:
fun isFavoriteAsync(item: FolderItem, callback: (Boolean)->Unit) {
viewModelScope.launch {
val isFavorite = getImageFolders().any { it.path == item.path }
callback(isFavorite)
}
}
and at the call site use it like
viewModel.isFavoriteAsync(myFolderItem) { isFavorite ->
// do something with return value when it's ready here
}
your getImageFolder() is an expensive function so
getImageFolders().value?.forEach {
if (it.path == item.path) {
retval = true
}
}
in this part the value is still null that is why it returns false.
the solution is to make sure the value is not null. Do not check null inside isFavorite function instead call isFavorite() function only when getImageFolder() is done the operation.
What you should do is something like this
observe the liveData of imageFolders
ondatachange check if the data is null or not
if it is not null update UI and use isFavourite() function
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.
Using Kotlin, Retrofit and Coroutines, I have defined an interface to get data from a remote server and most importantly pass the id of a selected RecyclerView item back to the server.
interface CourseService {
#GET("/mobile/feed/course_data.php")
suspend fun getCourseData(#Query("pathName") pathName: String): Response<List<Course>>
}
Here, i get the id of the selected item from a RecyclerView from my MainFragment and store it in "selectedItem" variable.
override fun onPathItemClick(path: Path) {
viewModel.selectedItem.value = path
selectedItem= viewModel.selectedItem.value!!.path_id
navController.navigate(R.id.action_mainFragment_to_courseFragment)
}
I pass the value of selected item to the getCourseData() function
class CourseRepository(val app: Application) {
val courseData = MutableLiveData<List<Course>>()
init {
CoroutineScope(Dispatchers.IO).launch {
callWebService()
}
}
#WorkerThread
suspend fun callWebService() {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
val serviceData = service.getCourseData(selectedItem).body() ?: emptyList()
courseData.postValue(serviceData)
}
}
But i get no results and it seems as though the value passed to getCourseData() function is null, but when checking the log is does have a value.
so if i give it a predefined value anywhere in my code like below, everything works completely fine
selectedItem= "MOB001"
val serviceData = service.getCourseData(selectedItem).body() ?: emptyList()
However, i cannot give it a fixed value prior to runtime because the value is retrieved when the user selects an item from a RecyclerView.
These are my multiple logs:
2020-05-01 13:56:30.431 23843-23843/ I/mylog: Main Fragment before item click: selectedItem =
2020-05-01 13:56:37.757 23843-23843/ I/mylog: Main Fragment after item click: selectedItem = WEB001
2020-05-01 13:56:37.763 23843-23843/ I/mylog: Course Fragment onCreateView(): selectedItem = WEB001
2020-05-01 13:56:37.772 23843-23901/ I/mylog: Course Fragment CourseRepository: selectedItem = WEB001
How can i overcome this issue?
You should call your CourseRepository's suspend function callWebService inside your ViewModel. Here is your repository:
class CourseRepository(val app: Application) {
suspend fun callWebService(path: Path): List<Course> {
return withContext(Dispatchers.IO) {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
service.getCourseData(path.path_id).body() ?: emptyList()
}
}
}
Then you should call your repository function in your ViewModel as follows:
fun getCourseData(path: Path): LiveData<List<Course>> {
val response = MutableLiveData<List<Course>>()
viewModelScope.launch {
response.postValue(repository.callWebService(path))
}
return response
}
Then call viewModel. getCourseData(path) from your Activity or Fragment or anywhere when you get valid Path value.
Don't forget to include implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file.
Your code seems to be correct, however, it is highly possible that your RecyclerView is being populated the first time and and evertime you go back and choose another path it is being populated with the same data and view.
Therefore, your attentions should be focused on why the data is not being fetched again, which is the cause of the RecyclerView and Fragment holding on to the same first view.
After days of thinking my code was wrong, it turned out that my RecyclerView adapter was loading the same view everytime i wen back to select a different path becuase my RecyclerView was being inflated in the onCreateView() function which is only called once only, when a fragment is inflated the first time.
class CourseFragment : Fragment(),
CourseRecyclerAdapter.CourseItemListener {
private lateinit var viewModel: CourseViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var navController: NavController
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_course, container, false)
recyclerView = view.findViewById(R.id.courseRecyclerView)
navController = Navigation.findNavController(requireActivity(), R.id.nav_host )
viewModel = ViewModelProvider(requireActivity()).get(CourseViewModel::class.java)
viewModel.courseData.observe(viewLifecycleOwner, Observer {
val adapter =
CourseRecyclerAdapter(
requireContext(),
it,
this
)
recyclerView.adapter = adapter
} )
return view
}
override fun onCourseItemClick(course: Course) {
viewModel.selectedCourse.value = course
navController.navigate(R.id.action_courseFragment_to_detailFragment)
}
}
I'm trying to display a fragment inside a recycler view. When you first load the fragment and return the view you get: E/RecyclerView: No adapter attached; skipping layout. This is because I actually don't have the adapter attached at first. I call the adapter in a function called createFirebaseListener() that listens for changes to the database and updates the recyclerview in real time. Here's the code:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.groups, container, false)
createFirebaseListener()
setupSendButton(view)
return view
}
private fun createFirebaseListener(){
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val toReturn: ArrayList<Message> = ArrayList();
for(data in dataSnapshot.children){
val messageData = data.getValue<Message>(Message::class.java)
//unwrap
val message = messageData?.let { it } ?: continue
toReturn.add(message)
}
//sort so newest at bottom
toReturn.sortBy { message ->
message.timestamp
}
setupAdapter(toReturn)
}
override fun onCancelled(databaseError: DatabaseError) {
//log error
}
}
mDatabase?.child("Group Chat")?.addValueEventListener(postListener)
}
private fun setupAdapter(data: ArrayList<Message>){
val linearLayoutManager = LinearLayoutManager(context)
mainActivityRecyclerView.layoutManager = linearLayoutManager
mainActivityRecyclerView.adapter = MessageAdapter(data){
Log.d("we.", "got here!")
}
//scroll to bottom
mainActivityRecyclerView.scrollToPosition(data.size - 1)
}
As you can see, in setupAdapter I update the recycler view each time a new message arrives. Since I am not getting the messages as the fragment loads, I get the error above on loading the fragment. But it also crashes the app if I change the orientation to landscape or if I load another fragment and then come back to this fragment: java.lang.IllegalStateException: mainActivityRecyclerView must not be null.
How should I best handle populating the recyclerview in onCreateView?
It is initialized in import
com.ntx_deisgns.cyberchatter.cyberchatter.R. The name
mainActivityRecyclerView is the id of my recyclerview
I believe it's not working because you are not initializing the RecyclerView inside the Fragment and it shows must not be null error. So, i'd suggest doing something like this:
Initializing & moving setupAdapter method's codes inside the
Fragment after inflating view and before return view.
Override onActivityCreated and moving setupAdapter method's codes inside it and then it should work.
Take a look: Android with Kotlin error when use RecyclerView in Fragment
P.s: Remember, use notifyDataSetChanged() after updating or setting
data to an adapter to avoid such errors.
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.