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
}
}
}
Related
I'm pretty new in the world of MVI pattern. So I'm trying to understand how fit together all the pieces.
I have an app that I structured using MVI pattern (or at least it was what I was meant to do). I have my fragment (I used navigation component but at the moment focus just on one fragment), which is supported by its own ViewModel. Then I have a repository class where all viewmodels retrieve data. Repository has 2 source of data, a web API and a local DB used as cache of data, I used Room for DB management.
I tried different approaches to the problem. At the moment I have done in this way:
In the DAO I used this instruction to retrieve data from the DB:
#Query("SELECT * FROM Users WHERE idTool=:idTool AND nickname LIKE '%' || :query || '%'")
fun users(idTool: Int, query: String) : Flow<List<User>>
Then in my repository I simple get this query to forward to ViewModels:
fun usersFlow(idTool: Int, query: String) = userDao.users(idTool, query)
In the ViewModel I created two MutableLiveData, coordinated by a MediatorLiveData:
val nicknameQuery = MutableStateFlow("")
private val nicknameQueryFlow = nicknameQuery.flatMapLatest {
repository.usersFlow(idToolQuery.value, it)
}
val idToolQuery = MutableStateFlow(DEFAULT_TOOL_ID)
private val idToolQueryFlow = idToolQuery.flatMapLatest {
repository.usersFlow(it, nicknameQuery.value)
}
val users = MediatorLiveData<List<User>>()
init {
users.addSource(nicknameQueryFlow.asLiveData()) {
users.value = it
}
users.addSource(idToolQueryFlow.asLiveData()) {
users.value = it
}
fetchUsers()
}
In this way, from my fragment, I can simply update nicknameQuery or idToolQuery to have an updated list in my RecyclerView. My first doubt is that in this way the fetch of data from my DB is done 2 times, one time for each mutable, but I'd like to retrieve data just one on the app opening (maybe the solution fro this is just check in the nicknameQuery that current query is different from the passed one, in this way since at the beginning current query is empty and it pass an empty query, it is bypassed).
In the Init method of ViewModel, I also call fetchUsers():
private fun fetchUsers() {
viewModelScope.launch {
repository.fetchUsers(DEFAULT_TOOL_ID).collect {
_dataState.value = it
}
}
}
This method checks into the database if there are already cached users with this specific idTool, if not it fetches them from the web and it stores retrieved data into the DB. This is the method inside my repository class:
suspend fun fetchUsers(
idTool: Int,
forceRefetch: Boolean = false
): Flow<DataState<List<User>>> = flow {
try {
var cachedUser = userDao.users(idTool, "").first()
val users: List<User>
if(cachedUser.isEmpty() || forceRefetch) {
Log.d(TAG, "Retrieve users: from web")
emit(DataState.Loading)
withContext(Dispatchers.IO) {
appJustOpen = false
val networkUsers =
api.getUsers(
idTool,
"Bearer ${sessionClient.tokens.accessToken.toString()}"
)
users = entityMapper.mapFromEntitiesList(networkUsers)
userDao.insertList(users)
}
} else {
users = cachedUser
}
emit(DataState.Success(users))
} catch (ex: Exception) {
emit(DataState.Error(ex))
}
}
This method checks if I have already users inside the DB with this specific idTool, if not it fetches them from API. It uses a DataState to update the UI, based on the result of the call. During the fetch of data, it emits a Loading state, this shows a progress bar in my fragment. If data is correctly fetched it emits a Success, and the fragment hides the progress bar to shows the recycler view. This is done in the following way. In my ViewModel I have this mutable state
private val _dataState = MutableLiveData<DataState<List<User>>>()
val dataState: LiveData<DataState<List<User>>> get() = _dataState
As you saw above, my fetch method is
private fun fetchUsers() {
viewModelScope.launch {
repository.fetchUsers(DEFAULT_TOOL_ID).collect {
_dataState.value = it
}
}
}
And finally in my fragment I have:
userListViewModel.dataState.observe(viewLifecycleOwner, { dataState ->
when (dataState) {
is DataState.Success -> {
showUserList()
}
is DataState.Error -> {
Log.e("TEST", dataState.exception.toString())
hideLoader()
Toast.makeText(activity, "Error retrieving data: ${dataState.exception}", Toast.LENGTH_LONG).show()
}
is DataState.Loading -> {
showLoader()
}
else -> {
// Do Nothing in any other case
}
}
})
At this moment Success state takes a list of users, but this list is there from a previous approach, at the moment it is useless since after data is fetched list is inserted into the DB, and I have a Flow to the DB which takes care to update the UI. In this way when I change idTool, when I change query, when I remove a user, the view is always notified
Is this approach correct?
Before this, I used another approach. I returned not a flow from my DB but just a List. Then my fetchUsers always returned a DataState<List>, it checked in the DB and if didn't found anything it fetched data from the web and returned that list. This approach caused me some problems, since every time I changed idTool or query, I always had to call fetchUsers method. Even if a user was removed from database, views didn't get notified since I didn't have a direct flow with the DB.
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 am creating demo project for using jetpack compose with mvvm , i have created model class that holds the list of users.. those users are displayed in list and there is a button at top which adds new user to the list when clicked...
when user clicks on the button an the lambda updates activity about it and activity calls viewmodel which adds data to list and updates back to activity using livedata, now after the model receives the new data it does not update composable function about it and hence ui of list is not updated..
here is the code
#Model
data class UsersState(var users: ArrayList<UserModel> = ArrayList())
Activity
class MainActivity : AppCompatActivity() {
private val usersState: UsersState = UsersState()
private val usersListViewModel: UsersListViewModel = UsersListViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
usersListViewModel.getUsers().observe(this, Observer {
usersState.users.addAll(it)
})
usersListViewModel.addUsers()
setContent {
UsersListUi.addList(
usersState,
onAddClick = { usersListViewModel.addNewUser() },
onRemoveClick = { usersListViewModel.removeFirstUser() })
}
}
}
ViewModel
class UsersListViewModel {
private val usersList: MutableLiveData<ArrayList<UserModel>> by lazy {
MutableLiveData<ArrayList<UserModel>>()
}
private val users: ArrayList<UserModel> = ArrayList()
fun addUsers() {
users.add(UserModel("jon", "doe", "android developer"))
users.add(UserModel("john", "doe", "flutter developer"))
users.add(UserModel("jonn", "dove", "ios developer"))
usersList.value = users
}
fun getUsers(): MutableLiveData<ArrayList<UserModel>> {
return usersList
}
fun addNewUser() {
users.add(UserModel("jony", "dove", "ruby developer"))
usersList.value = users
}
fun removeFirstUser() {
if (!users.isNullOrEmpty()) {
users.removeAt(0)
usersList.value = users
}
}
}
composable function
#Composable
fun addList(state: UsersState, onAddClick: () -> Unit, onRemoveClick: () -> Unit) {
MaterialTheme {
FlexColumn {
inflexible {
// Item height will be equal content height
TopAppBar( // App Bar with title
title = { Text("Users") }
)
FlexRow() {
expanded(flex = 1f) {
Button(
text = "add",
onClick = { onAddClick.invoke() },
style = OutlinedButtonStyle()
)
}
expanded(flex = 1f) {
Button(
text = "sub",
onClick = { onRemoveClick.invoke() },
style = OutlinedButtonStyle()
)
}
}
VerticalScroller {
Column {
state.users.forEach {
Column {
Row {
Text(text = it.userName)
WidthSpacer(width = 2.dp)
Text(text = it.userSurName)
}
Text(text = it.userJob)
}
Divider(color = Color.Black, height = 1.dp)
}
}
}
}
}
}
}
the whole source code is available here
I am not sure if i am doing something wrong or is it because jetpack compose is still in developers preview , so would appreciate any help..
thank you
Ahoy!
Sean from Android Devrel here. The main reason this isn't updating is the ArrayList in UserState.users is not observable – it's just a regular ArrayList so mutating it won't update compose.
Model makes all properties of the model class observable
It seems like this might work because UserState is annotated #Model, which makes things automatically observable by Compose. However, the observability only applies one level deep. Here's an example that would never trigger recomposition:
class ModelState(var username: String, var email: String)
#Model
class MyImmutableModel(val state: ModelState())
Since the state variable is immutable (val), Compose will never trigger recompositions when you change the email or username. This is because #Model only applies to the properties of the class annotated. In this example state is observable in Compose, but username and email are just regular strings.
Fix Option #0: You don't need #Model
In this case you already have a LiveData from getUsers() – you can observe that in compose. We haven't shipped a Compose observation yet in the dev releases, but it's possible to write one using effects until we ship a observation method. Just remember to remove the observer in onDispose {}.
This is also true if you're using any other observable type, like Flow, Flowable, etc. You can pass them directly into #Composable functions and observe them with effects without introducing an intermediate #Model class.
Fix Option #1: Using immutable types in #Model
A lot of developers prefer immutable data types for UI state (patterns like MVI encourage this). You can update your example to use immutable lists, then in order to change the list you'll have to assign to the users property which will be observable by Compose.
#Model
class UsersState(var users: List<UserModel> = listOf())
Then when you want to update it you have to assign the users variable:
val usersState = UsersState()
// ...
fun addUsers(newUsers: List<UserModel>) {
usersState.users = usersState.users + newUsers
// performance note: note this allocates a new list every time on the main thread
// which may be OK if this is rarely called and lists are small
// it's too expensive for large lists or if this is called often
}
This will always trigger recomposition any time a new List<UserModel is assigned to users, and since there's no way to edit the list after it's been assigned the UI will always show the current state.
In this case, since the data structure is a List that you're concatenating the performance of immutable types may not be acceptable. However, if you're holding an immutable data class this option is a good one so I included it for completeness.
Fix Option #2: Using ModelList
Compose has a special observable list type for exactly this use case. You can use instead of an ArrayList and any changes to the list will be observable by compose.
#Model
class UsersState(val users: ModelList<UserModel> = ModelList())
If you use ModelList the rest of the code you've written in the Activity will work correctly and Compose will be able to observe changes to users directly.
Related: Nesting #Model classes
It's worth noting that you can nest #Model classes, which is how the ModelList version works. Going back to the example at the beginning, if you annotate both classes as #Model, then all of the properties will be observable in Compose.
#Model
class ModelState(var username: String, var email: String)
#Model
class MyModel(var state: ModelState())
Note: This version adds #Model to ModelState, and also allows reassignment of state in MyModel
Since #Model makes all of the properties of the class that is annotated observable by compose, state, username, and email will all be observable.
TL;DR which option to choose
Avoiding #Model (Option #0) completely in this code will avoid introducing a duplicate model layer just for Compose. Since you're already holding state in a ViewModel and exposing it via LiveData you can just pass the LiveData directly to compose and observe it there. This would be my first choice.
If you do want to use #Model to represent a mutable list, then use ModelList from Option #2.
You'll probably want to change the ViewModel to hold a MutableLiveData reference as well. Currently the list held by the ViewModel is not observable. For an introduction to ViewModel and LiveData from Android Architecture components check out the Android Basics course.
Your model is not observed so changes won't be reflected.
In this article under the section 'Putting it all together' the List is added.
val list = +memo{ calculation: () -> T}
Example for your list:
#Composable
fun test(supplier: UserState) {
val list = +memo{supplier.users}
ListConsumer(list){
/* Do other stuff for your usecase */
}
}
emit accepts the data class whereas emitSource accepts LiveData<T> ( T -> data ). Considering the following example :- I have two type of calls :-
suspend fun getData(): Data // returns directly data
and the other one ;
suspend fun getData(): LiveData<Data> // returns live data instead
For the first case i can use:-
liveData {
emit(LOADING)
emit(getData())
}
My question : Using the above method would solve my problem , WHY do we need emitSource(liveData) anyway ?
Any good use-case for using the emitSource method would make it clear !
As you mentioned, I don't think it solves anything in your stated problem, but I usually use it like this:
If I want to show cached data to the user from the db while I get fresh data from remote, with only emit it would look something like this:
liveData{
emit(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
emit(db.getData())
}
But with emitSource it looks like this:
liveData{
emitSource(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
}
Don't need to call emit again since the liveData already have a source.
From what I understand emit(someValue) is similar to myData.value = someValue whereas emitSource(someLiveValue) is similar to myData = someLiveValue. This means that you can use emit whenever you want to set a value once, but if you want to connect your live data to another live data value you use emit source. An example would be emitting live data from a call to room (using emitSource(someLiveData)) then performing a network query and emitting an error (using emit(someError)).
I found a real use-case which depicts the use of emitSource over emit which I have used many times in production now. :D The use-case:
Suppose u have some user data (User which has some fields like userId, userName ) returned by some ApiService.
The User Model:
data class User(var userId: String, var userName: String)
The userName is required by the view/activity to paint the UI. And the userId is used to make another API call which returns the UserData like profileImage , emailId.
The UserData Model:
data class UserData(var profileImage: String, var emailId: String)
This can be achieved internally using emitSource by wiring the two liveData in the ViewModel like:
User liveData -
val userLiveData: LiveData<User> = liveData {
emit(service.getUser())
}
UserData liveData -
val userDataLiveData: LiveData<UserData> = liveData {
emitSource(userLiveData.switchMap {
liveData {
emit(service.getUserData(it.userId))
}
})
}
So, in the activity / view one can ONLY call getUser() and the getUserData(userId) will be automatically triggered internally via switchMap.
You need not manually call the getUserData(id) by passing the id.
This is a simple example, imagine there is a chain of dependent-tasks which needs to be executed one after the other, each of which is observed in the activity. emitSource comes in handy
With emitSource() you can not only emit a single value, but attach your LiveData to another LiveData and start emitting from it. Anyway, each emit() or emitSource() call will remove the previously added source.
var someData = liveData {
val cachedData = dataRepository.getCachedData()
emit(cachedData)
val actualData = dataRepository.getData()
emitSource(actualData)
}
The activity that’s observing the someData object, will quickly receive the cached data on the device and update the UI. Then, the LiveData itself will take care of making the network request and replace the cached data with a new live stream of data, that will eventually trigger the Activity observer and update the UI with the updated info.
Source: Exploring new Coroutines and Lifecycle Architectural Components integration on Android
I will like share a example where we use "emit" and "emitsource" both to communicate from UI -> View Model -> Repository
Repository layer we use emit to send the values downstream :
suspend fun fetchNews(): Flow<Result<List<Article>>> {
val queryPath = QueryPath("tata", apikey = AppConstant.API_KEY)
return flow {
emit(
Result.success(
openNewsAPI.getResponse(
"everything",
queryPath.searchTitle,
queryPath.page,
queryPath.apikey
).articles
)
)
}.catch { exception ->
emit(Result.failure(RuntimeException(exception.message)));
}
}
ViewModel layer we use emitsource to pass the live data object to UI for subscriptions
val loader = MutableLiveData<Boolean>()
val newsListLiveData = liveData<Result<List<Article>>> {
loader.postValue(true)
emitSource(newRepo.fetchNews()
.onEach {
loader.postValue(false)
}
.asLiveData())
}
UI Layer - we observe the live data emitted by emitsource
viewModel.newsListLiveData.observe(viewLifecycleOwner, { result ->
val listArticle = result.getOrNull()
if (result.isSuccess && listArticle != null) {
setupList(binding.list, listArticle)
} else {
Toast.makeText(
appContext,
result.exceptionOrNull()?.message + "Error",
Toast.LENGTH_LONG
).show()
}
})
We convert Flow observable to LiveData in viewModel
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!