my app architecture, quite common:
Please explain me if I have list of Entities, for example
#Entity(tableName = TABLE_NAME)
class Item constructor(
#PrimaryKey(autoGenerate = false)
var id: Long = 0L,
#TypeConverters(ListToStringConverter::class)
var eventDescription: List<String> = emptyList(),
#TypeConverters(DateConverter::class)
var date: Date = Date(),
var rate: Int ?= null)
Picture explanation:
Currently I do (according to picture above):
mLiveData getLiveData from Repository
callbackrefreshFromDataBase()
mLiveData.addSource from LiveData of DataBase - what causes that Android View is updated quickly
callback refreshFromNetwork()
Rest updates DatabaseTable
Database insert causes that LiveData add pushes changes to the View
Formulation of the problem
What is the best practice for 5 step - a moment when new data comes and I have to replace old data with the freshest?
Currently I'm using RxJava, Room, Kotlin and I'm using in step 3 nested Rx.Single which is very ugly spagetti code.
like
disposable = usecase.getDataFromNetwork()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onSuccess = {
val itemsList = it
dataBase.deleteAllItems()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy (onComplete = { dataBase.insertNewItems(itemsList)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onComplete = {
// Process complete
}, onError = {
...
})
}
}, onError = {
...
})
}, onError = {
...
})
Ugly spagetti code.
QUESTIONS
Should I wipe all Table before inserting new Items? Livedata listen to the changes of List, so whenever anything changes (some item updated, some inserted, some removed) view will be updated
Under Circumstances where all my Streams are Single (reason: they dispose by itself when onComplete), how to chain them sequentially in oder to restrict one stream rule?
This depends if your fetched data will be similar or not. If you fresh data is normally completly different I would delete all elements and then insert new elements with only one method like following:
#Insert
public void insertItems(Item... items);
If your new data is similar and you don't care about their order I would compare all new items to the old ones to know for each item if you have to update, insert or delete item. At the end you call deleteItems(itemsToDelete), updateItems(itemsToUpdate) and insertItems(itemsToInsert). This three methods are similar as the one written above but with #Delete, #Update and #Insert.
If you care about the order I recommend you take a look at Android Paging Library and create a custom DataSource where you can tell to your live data when to update its values. Some links to start: this, this, this and this
Related
My use case is as follows:
Imagine that there is an Android Fragment that allows users to search for Grocery items in a store. There's a Search View, and as they type, new queries are sent to the Grocery item network service to ask for which items match the query. When successful, the query returns a list of Grocery items that includes the name, price, and nutritional information about the product.
Locally on the Android device, there is a list of known for "items for sale" stored in a raw file. It's in the raw resources directory and is simply a list of grocery item names and nothing else.
The behavior we wish to achieve is that as the user searches for items, they are presented with a list of items matching their query and a visual badge on the items that are "For Sale"
The constraints I am trying to satisfy are the following:
When the user loads the Android Fragment, I want to parse the raw text file asynchronously using a Kotlin coroutine using the IO Dispatcher. Once parsed, the items are inserted into the Room database table for "For Sale Items" which is just a list of names where the name is the primary key. This list could be empty, it could be large (i.e. >10,0000).
Parallel, and independent of #1, as the user types and makes different queries, I want to be sending out network requests to the server to retrieve the Grocery Items that match their query. When the query comes back successfully, these items are inserted into a different table in the Room database for Grocery Items
Finally, I only want to render the list returned from #2 once I know that the text file from #1 has been successfully parsed. Once I know that #1 has been successfully parsed I want to join the tables in the database on name and give that LiveData to my ViewModel to render the list. If either #1 or #2 fail, I want the user to be given an "Error occurred, Retry" button
Where I am struggling right now:
Seems achievable by simply kicking off a coroutine in ViewModel init that uses the IO Dispatcher. This way I only attempt to parse the file once per ViewModel creation (I'm okay with reparsing it if the user kills and reopens the app)
Seems achievable by using another IO Dispatcher coroutine + Retrofit + Room.
Satisfying the "Only give data to ViewModel when both #1 and #2 are complete else show error button" is the tricky part here. How do I expose a LiveData/Flow/something else? from my Repository that satisfies these constraints?
When you launch coroutines, they return a Job object that you can wait for in another coroutine. So you can launch a Job for 1, and 3 can await it before starting its flow that joins tables.
When working with Retrofit and Room, you can define your Room and Retrofit DAOs/interfaces with suspend functions. This causes them to generate implementations that internally use an appropriate thread and suspend (don't return) until the work of inserting/updating/fetching is complete. This means you know that when your coroutine is finished, the data has been written to the database. It also means it doesn't matter which dispatcher you use for 2, because you won't be calling any blocking functions.
For 1, if parsing is a heavy operation, Dispatchers.Default is more appropriate than Dispatchers.IO, because the work will truly be tying up a CPU core.
If you want to be able to see if the Job from 1 had an error, then you actually need to use async instead of launch so any thrown exception is rethrown when you wait for it in a coroutine.
3 can be a Flow from Room (so you'd define the query with the join in your DAO), but you can wrap it in a flow builder that awaits 1. It can return a Result, which contains data or an error, so the UI can show an error state.
2 can operate independently, simply writing to the Room database by having user input call a ViewModel function to do that. The repository flow used by 3 will automatically pick up changes when the database changes.
Here's an example of ViewModel code to achieve this task.
private val parsedTextJob = viewModelScope.async(Dispatchers.Default) {
// read file, parse it and write to a database table
}
val theRenderableList: SharedFlow<Result<List<SomeDataType>>> = flow {
try {
parsedTextJob.await()
} catch (e: Exception) {
emit(Result.failure(e)
return#flow
}
emitAll(
repository.getTheJoinedTableFlowFromDao()
.map { Result.success(it) }
)
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
fun onNewUserInput(someTextFromUser: String) {
viewModelScope.launch {
// Do query from Retrofit.
// Parse results and write to database.
}
}
If you prefer LiveData to SharedFlow, you can replace theRenderableList above with:
val theRenderableList: LiveData<Result<List<SomeDataType>>> = liveData {
try {
parsedTextJob.await()
} catch (e: Exception) {
emit(Result.failure(e)
return#liveData
}
emitSource(
repository.getTheJoinedTableFlowFromDao()
.map { Result.success(it) }
.asLiveData()
)
}
You could do this by having the ViewModel monitor when the two tasks are complete and set loading state LiveData variable to indicate that the UI should only update once both tasks are complete. For example:
class MainViewModel : ViewModel() {
private var completedA = false
private var completedB = false
private val dataALiveData = MutableLiveData("")
val dataA: LiveData<String>
get() = dataALiveData
private val dataBLiveData = MutableLiveData("")
val dataB: LiveData<String>
get() = dataBLiveData
private val dataIsReadyLiveData = MutableLiveData(false)
val dataIsReady: LiveData<Boolean>
get() = dataIsReadyLiveData
// You can trigger a reload of some of this data without having to reset
// any flags - the UI will be updated when the task is complete
fun reloadB() {
viewModelScope.launch { doTaskB() }
}
private suspend fun doTaskA() {
// Fake task A - once it's done post relevant data
// (if applicable), indicate that it is completed, and
// check if the app is ready
delay(3200)
dataALiveData.postValue("Data A")
completedA = true
checkForLoaded()
}
private suspend fun doTaskB() {
// Fake task B - once it's done post relevant data
// (if applicable), indicate that it is completed, and
// check if the app is ready
delay(2100)
dataBLiveData.postValue("Data B")
completedB = true
checkForLoaded()
}
private fun checkForLoaded() {
if( completedA && completedB ) {
dataIsReadyLiveData.postValue(true)
}
}
// Launch both coroutines upon creation to start loading
// the two data streams
init {
viewModelScope.launch { doTaskA() }
viewModelScope.launch { doTaskB() }
}
}
The activity or fragment could observe these three sets of LiveData to determine what to show and when, for example to hide the displayed elements and show a progress bar or loading indicator until it is done loading both.
If you wanted to handle error states, you could have the dataIsReady LiveData hold an enum or string to indicate "Loading", "Loaded", or "Error".
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val model: MainViewModel by viewModels()
binding.textA.visibility = View.INVISIBLE
binding.textB.visibility = View.INVISIBLE
binding.progressBar.visibility = View.VISIBLE
model.dataA.observe(this) { data ->
binding.textA.text = data
}
model.dataB.observe(this) { data ->
binding.textB.text = data
}
// Once the data is ready - change the view visibility state
model.dataIsReady.observe(this) { isReady ->
if( isReady ) {
binding.textA.visibility = View.VISIBLE
binding.textB.visibility = View.VISIBLE
binding.progressBar.visibility = View.INVISIBLE
// alternately you could read the data to display here
// by calling methods on the ViewModel directly instead of
// having separate observers for them
}
}
}
In my viewModel I have:
private val setEntitiesList = mutableStateListOf<Exercise>()
val exercisesFromDB = exerciseDao.getAllExercisesWithSetNo(trainingId)
val exercises =
exercisesFromDB.combine(setEntitiesList.asFlow()) { exercises, setEntitiesList ->
Pair(exercises, setEntitiesList)
}.mapLatest { (exercises, setEntitiesList) ->
//I am altering exercises list here
exercises
}
In my fragment I have:
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED){
addTrainingViewModel.exercises.collectLatest {
exercisesAdapter.submitList(it)
}
}
}
It doesnt work. Nothing is collected in fragment
If I change in fragment to collect "addTrainingViewModel.exercisesFromDB" it works - values are emited and collected.
What I would like to achieve: new list of exercises is emitted when setEntitiesList or exercisesFromDB are changed and I am able to do sth with a list of exercises before it is emitted
I assume that setEntitiesList variable is of type Iterable. In that case when you convert it to Flow like setEntitiesList.asFlow() you create a cold Flow, it means that when you add new elements to the setEntitiesList iterable they won't be emitted to the combined Flow.
On the other hand, by calling addTrainingViewModel.exercisesFromDB you get a hot Flow, so when you tested it separately you got the right result.
So you can't use a converted iterable object setEntitiesList as hot Flow. You need somehow to rewrite your logic and use a hot Flow instead of cold Flow. You can use MutableSharedFlow for that. The code will be something like the following:
private val setEntitiesListFlow = MutableSharedFlow<Exercise>(extraBufferCapacity = 64)
val exercisesFromDB = exerciseDao.getAllExercisesWithSetNo(trainingId)
val exercises =
exercisesFromDB.combine(setEntitiesListFlow) { exercises, setEntitiesList ->
Pair(exercises, setEntitiesList)
}.mapLatest { (exercises, setEntitiesList) ->
//I am altering exercises list here
exercises
}
// somewhere else in your code emitting Exercise:
setEntitiesListFlow.tryEmit(Exercise(...))
I am working my through a new android application using Jetpack Compose (1.0.0-alpha08) and RxJava2 to manage the flow of data from my model (Realm 10 in this case. For a given screen, I have a view model that defines the data that will be subscribed to by the top level Compostable view. So, for example:
ViewModel...
class ListItemViewModel: ViewModel() {
val items: Flowable<Item>
get() {
val data1 = userRealm.where<Item1>()
.also(query).findAllAsync().asFlowable()
.onBackpressureLatest().doOnNext{System.out.println("Realm on Next")}
.observeOn(
Schedulers.single()
).filter{it.isLoaded}.map{ result ->
System.out.println("Maping Realm")
result
}.doOnSubscribe {System.out.println("Subscribe")}
val data2 == //same as above but with a different item
return Flowable.combineLatest(data1, data2, combineFunction)
.onBackpressureLatest()
.doOnNext{System.out.println("Hello")}
.doOnComplete {System.out.println("Complete")}
.subscribeOn(AndroidSchedulers.mainThread())
}
}
View
#Compostable
fun List(List<Item> items) {
val viewModel: ListItemViewModel = viewModel()
val list by viewModel.items.subscribeAsState(initial = listOf())
ItemList(list = list)
}
#Compostable
fun ItemList(List<Item> items {
LazyColumnFor(...) {
.......
}
}
Everything works as I would expect and the list renders on the screen as I want. However, what I assume would happen here is that the subscribe would only happen once and the Flowable would only push out new data as new data was emitted. As a result, I would only expect the various onNext methods to be triggered when new data was present in the stream, e.g. something changed in the realm db. As I am not adding/deleting any data to/from the Realm, once I have the first set of results, I would expect the flowable to go "silent".
However, when I run the above, the subscribe message related to the realm subscription is logged over and over. The same for the "Hello" and the other logging statements in the onNext methods. Also, if I add any logging in my combine function, I see those log statements in the same fashion as I see the "Hello" log. From this it seems like each time the List composable is being rendered, it resubscribes to the Flowable from my viewmodel and triggers the full process. As I said, I was expecting that this subscription would only happen once.
This is perhaps correct behaviour, but mentally, it feels like I am burning CPU cycles for no reason as my methods are being called over and over when no data has change. Am I setting things up correctly, or is there something flawed in how I have configured things?
I ultimately worked around the problem and took a hybrid approach where I used Realm/RXJava to handle the data flow and when things have changed, update a LiveData object.
View Model
private val internalItemList = MutableLiveData(listOf<Item>())
val itemList: LiveData<List<Item>> = internalItemList
//capture the subscription so you can dispose in onCleared()
val subscription = items.observeOn(AndroidSchedulers.mainThread()).subscribe {
this.internalItemList.value = it
}
View
val list by viewModel.itemList.observeAsState(listOf())
This is must less chatty and works as I want it to. Not sure if it is the correct way to do this, but it seems to be working
I'm working on Messaging app and I am implementing database + network to save chat messages from api and show them from database. I'm using BoundaryCallback to fetch message when database has no more data. My api works like this:
getlist( #Query("msgid") long msgid,
#Query("loadolder") boolean olderOrNewer,
#Query("showcurrentMessage") boolean showcurrentMessage,
#Query("MsgCountToLoad") int MsgCountToLoad);
msgid : last message id of that chat . if db is empty I request
with the chat.getlastmessageid if the db has data but there was no
more data I will send last message id in db to load more and if first
time opening chat, the last message id in db was not equal to
chat.lastmessageid it's mean there is new message to load.
loadolder : this flag false to tell api load new messages from this
message id I sent to you and on and if flag set to true means load
older messages from this message id I sent to you
showcurrentMessage: if true it will give me the current message (msgid) too
MsgCountToLoad : how many messages to take from api
question is how to handle this stuff in Pagginglibrary? How to tell it to load older or newer message which is based on scrolling position. First time to load data is easy, it will returns null object so I will use the chat.lastmessageid next time opening chat where I could check if chat.lastmessageid is equal to db.lastmessageid and tell it to load more new messages.
PagedList.BoundaryCallback has two separate APIs for prepending and appending.
You should look to implement these methods:
onItemAtEndLoaded
onItemAtFrontLoaded
Assuming your initial load loads the most recent messages, and scrolling up loads older messages, you can just pass true for loadolder in onItemAtFrontLoaded and false in onItemAtEndLoaded.
I'm working my last project on Messaging app. One the most important and common things that we do in our projects is to load data gradually from the network or the database maybe because there is a huge number of entities that can’t be loaded at once.
If you are not familiar with paging library or live data concepts please take your time to study them first because I’m not going to talk about them here. There are lots of resources you can use to learn them.
My solution consists of two main parts!
Observing the database using Paging Library.
Observing the RecyclerView to understand when to request the server for data pages.
For demonstration we are going to use an entity class that represents a Person:
#Entity(tableName = "persons")
data class Person(
#ColumnInfo(name = "id") #PrimaryKey val id: Long,
#ColumnInfo(name = "name") val name: String,
#ColumnInfo(name = "update_time") val updateTime: Long
)
1. Observe the database
Lets start with the first and easier one:
To observe the database we are going to define a method in our dao that returns a DataSource.Factory<Int, Person>
#Dao
interface PersonDao {
#Query("SELECT * FROM persons ORDER BY update_time DESC")
fun selectPaged(): DataSource.Factory<Int, Person>
}
And now in our ViewModel we are going to build a PagedList from our factory
class PersonsViewModel(private val dao: PersonDao) : ViewModel() {
val pagedListLiveData : LiveData<PagedList<Person>> by lazy {
val dataSourceFactory = personDao.selectPaged()
val config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.build()
LivePagedListBuilder(dataSourceFactory, config).build()
}
}
And from our view we can observe the paged list
class PersonsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_persons)
viewModel.pagedListLiveData.observe(this, Observer{
pagedListAdapter.submitList(it)
})
}
}
Alright, now that is basically what we should do for the first part. Please notice that we are using a PagedListAdapter. Also we can do some more customization on our PagedList.Config object but for simplicity we omit it. Again please notice that we didn’t use a BoundaryCallback on our LivePagedListBuilder.
2. Observe the RecyclerView
Basically what we should do here is to observe the list, and based on where in the list we are right now, ask the server the provide us with the corresponding page of data. For observing the RecyclerView position we are going to use a simple library called Paginate.
class PersonsActivity : AppCompatActivity(), Paginate.Callbacks {
private var page = 0
private var isLoading = false
private var hasLoadedAllItems = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_persons)
viewModel.pagedListLiveData.observe(this, Observer{
pagedListAdapter.submitList(it)
})
Paginate.with(recyclerView, this).build()
}
override fun onLoadMore() {
// send the request to get corresponding page
}
override fun isLoading(): Boolean = isLoading
override fun hasLoadedAllItems(): Boolean = hasLoadedAllItems
}
As you can see we bound the Paginate with the recycler view and now we have three callbacks. isLoading() should return the state of network. hasLoadedAllItems() shows whether or not we have reached the last page and there is no more data to load from the server. Most of what we do is implementing the last method onLoadMore().
In this stage we should do three things:
Based on the recyclerView position, we ask the server to present us with the right data page.
Using the fresh data from the server we update the database, resulting in updating the PagedList and showing the fresh data. Don’t forget we are observing the database!
If the request fails we show the error.
With these simple steps we solve two problems.
First of all despite the BoundaryCallbak, that doesn’t have a callback to fetch the already fetched data, we are requesting each page on demand so we can notice the updated entities and also update our own local databse.
Second we can easily show the state of the network and also show the possible network failures.
Sounds fine right? Well we haven’t solved one particular problem yet. And that is what if one entity gets deleted from the remote server. How are we going to notice that! Well that is where ordering of data comes in. With a really old trick of sorting the data we can notice the gaps between our persons. For example we can sort our persons based on their update_time now if the returned JSON page from the server looks like this:
{
"persons": [{
"id": 1,
"name": "Reza",
"update_time": 1535533985000
}, {
"id": 2,
"name": "Nick",
"update_time": 1535533985111
}, {
"id": 3,
"name": "Bob",
"update_time": 1535533985222
}, {
"id": 4,
"name": "Jafar",
"update_time": 1535533985333
}, {
"id": 5,
"name": "Feryal",
"update_time": 1535533985444
}],
"page": 0,
"limit": 5,
"hasLoadedAllItems": false
}
Now we can be sure that whether if there is a person in our local database that its update_time is between the first and the last person of this list, but it is not among these persons, is in fact deleted from the remote server and thus we should delete it too.
I hope I was too vague but take a look at the code below
override fun onLoadMore() {
if (!isLoading) {
isLoading = true
viewModel.loadPersons(page++).observe(this, Observer { response ->
isLoading = false
if (response.isSuccessful()) {
hasLoadedAllItems = response.data.hasLoadedAllItems
} else {
showError(response.errorBody())
}
})
}
}
But the magic happens in the ViewModel
class PersonsViewModel(
private val dao: PersonDao,
private val networkHelper: NetworkHelper
) : ViewModel() {
fun loadPersons(page: Int): LiveData<Response<Pagination<Person>>> {
val response =
MutableLiveData<Response<Pagination<Person>>>()
networkHelper.loadPersons(page) {
dao.updatePersons(
it.data.persons,
page == 0,
it.hasLoadedAllItems)
response.postValue(it)
}
return response
}
}
As you can see we emit the network result and also update our database
#Dao
interface PersonDao {
#Transaction
fun updatePersons(
persons: List<Person>,
isFirstPage: Boolean,
hasLoadedAllItems: Boolean) {
val minUpdateTime = if (hasLoadedAllItems) {
0
} else {
persons.last().updateTime
}
val maxUpdateTime = if (isFirstPage) {
Long.MAX_VALUE
} else {
persons.first().updateTime
}
deleteRange(minUpdateTime, maxUpdateTime)
insert(persons)
}
#Query("DELETE FROM persons WHERE
update_time BETWEEN
:minUpdateTime AND :maxUpdateTime")
fun deleteRange(minUpdateTime: Long, maxUpdateTime: Long)
#Insert(onConflict = REPLACE)
fun insert(persons: List<Person>)
}
Here in our dao first we delete all the persons that their updateTime is between the first and last person in the list returned from the server and then insert the list into the database. With that we made sure any person that is deleted on the server is also deleted in our local database as well. Also notice that we are rapping these two method calls inside a database #Transaction for better optimization. The changes of the database will be emitted through our PagedList thus updating the ui and with that we are done.
I'm making an Android app using Kotlin with Firebase products. I have successful connections with Firestore and can successfully retrieve the data I want, but I am having difficulty displaying it within a RecyclerView.
When the application loads, and after a user has logged in, my Firestore queries use the UID of the user to get a list of their assignments. Using logs I can see that this occurs without issue as the home screen loads. Within the home screen fragment I have data binding for the RecyclerView and setup my ViewModel to have the fragment observe the returned Firestore data.
I believe it is a misunderstanding on my part on exactly how LiveData works because if I tap the bottom nav icon for the home screen to trigger a refresh of the UI then the list populates and I can use the app as desired. Therefore my observer/LiveData must not be setup properly as it is not automatically refreshing once the data has changed (null list to not null list).
As I'm new to programming I'm sure I've fallen into a number of pitfalls and done a few things incorrectly, but I've been searching through StackOverflow and YouTube for help on this issue for months now. Unfortunately I don't have all of the links saved to every video and every post.
I've tried tweaking the ViewModel and the Repository/Database class (singleton) to different effects and currently I'm at my best version with only a single tap required to refresh the UI. Previously it took multiple taps.
from the Database class
private val assignments = MutableLiveData<List<AssignmentModel>>()
private fun getUserAssignments(c: ClassModel) {
val assignmentQuery = assignmentRef.whereEqualTo("Class_ID", c.Class_ID)
assignmentQuery.addSnapshotListener { documents, _ ->
documents?.forEach { document ->
val a = document.toObject(AssignmentModel::class.java)
a.Assignment_ID = document.id
a.Class_Title = c.Title
a.Formatted_Date_Due = formatAssignmentDueDate(a)
assignmentMap[a.Assignment_ID] = a
}
}
}
fun getAssignments() : LiveData<List<AssignmentModel>> {
assignments.value = assignmentMap.values.toList().filter {
if (it.Date_Due != null) it.Date_Due!!.toDate() >= Calendar.getInstance().time else true }
.sortedBy { it.Date_Due }
return assignments
}
from the ViewModel
class AssignmentListViewModel internal constructor(private val myDatabase: Database) : ViewModel() {
private var _assignments: LiveData<List<AssignmentModel>>? = null
fun getAssignments() : LiveData<List<AssignmentModel>> {
var liveData = _assignments
if (liveData == null) {
liveData = myDatabase.getAssignments()
_assignments = liveData
}
return liveData
}
}
from the Fragment
class AssignmentList : Fragment() {
private lateinit var model: AssignmentListViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = AssignmentListBinding.inflate(inflater, container, false)
val factory = InjectorUtils.provideAssignmentListViewModelFactory()
model = ViewModelProvider(this, factory).get(AssignmentListViewModel::class.java)
val assignmentAdapter = AssignmentAdapter()
binding.assignmentRecycler.adapter = assignmentAdapter
updateUI(assignmentAdapter)
return binding.root
}
private fun updateUI(adapter: AssignmentAdapter) {
model.getAssignments().observe(this, Observer { assignments ->
if (assignments.isNotEmpty()) adapter.submitList(assignments)
})
}
}
Again, I expect the RecyclerView to populate automatically once the data from Firestore appears, but it doesn't. The screen remains empty until I tap the home screen button.
These snippets show the most recent changes I've made. Originally I had the Firestore query function returning the LiveData directly. I also had a much simpler ViewModel of something like fun getAssignments() = myDatabase.getAssignments().
Thanks for any and all help and advice.
When troubleshooting this issue, I'd recommend starting by looking at two things.
Take a look at where/when you're updating your LiveData
The goal is whenever the data in Firebase updates, your assignments LiveData updates your UI. Something like:
Firestore updates
Firestore triggers SnapshotListener
SnapshotListener updates LiveData
LiveData observer updates UI
So in your snapshot listener, you should be updating your LiveData, which is what I think you're missing. So it would be something like:
// Where you define your SnapshotListner
assignmentQuery.addSnapshotListener { documents, _ ->
// Process the data
documents?.forEach { document ->
val a = document.toObject(AssignmentModel::class.java)
a.Assignment_ID = document.id
a.Class_Title = c.Title
a.Formatted_Date_Due = formatAssignmentDueDate(a)
assignmentMap[a.Assignment_ID] = a
}
// Update your LiveData
assignments.value = assignmentMap.values.toList().filter {
if (it.Date_Due != null) it.Date_Due!!.toDate() >= Calendar.getInstance().time else true }
.sortedBy { it.Date_Due }
}
Now every time your Firestore updates, your LiveData will update and your UI should update.
Given the code change, getAssignments() can just return assignments. You can do this using a Kotlin backing property, covered here:
private val _assignments = MutableLiveData<List<AssignmentModel>>()
val assignments: LiveData<List<AssignmentModel>>
get() = _assignments
As for why it's not working at the moment, right now you call getAssignments() once on start up. This will filter an empty assignmentMap.values (I believe - might be worth checking), because when it's called, Firebase hasn't finished getting you any data. And when Firebase does get it's new data, it triggers the listener, but you don't update the LiveData.
Mind where you're setting up your listeners/observers
A tricky thing with LiveData observers and Firebase listeners is to make sure you only set them up once.
For your Firebase listener, you should be setting up the listener when you initialize your database and not every single time you call getUserAssignments. Then you wouldn't need all the null checking in the ViewModel, which essentially ensures that at least the ViewModel won't call getUserAssignments twice....but if you have other classes interacting with your database, they might call getUserAssignments multiple times and then you have tons of extra listeners.
Also, make sure you detach your listener.
One way to handle this is described in Doug Stevenson's talk Firebase and Android Jetpack: Fit Like a Glove - the talk includes a demo code here. The part that's related to this is how he handles LiveData -- notice how the class includes adding and removing the listener. The TL;DR is that he's using LiveData's lifecycle awareness to automatically do Firebase listener setup and cleanup. How that's done is a bit complicated, so I'd suggest watching the talk from here.
For your LiveData, setup/tear down looks correct since it's getting setup in onCreateView (and torn down automatically via the fact it's lifecycle aware). I might rename updateUI to something like setupUIObservation, since updateUI sounds like something you call multiple times. As with the Firebase listeners, you want to make sure you're not setting up the same LiveData observer more than once.
Hope that helps!