Android, MVI pattern + Kotlin Flow + Room + Retrofit, how fit them together? - android

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.

Related

Emit Article with title Kotlin Flow

My task is to get whole Article with provided title from RecyclerView.
When I click on specific Article i get title from it.
Room database:
#Query("SELECT * FROM article_table WHERE title = :title")
fun getArticleDetails(title: String): Flow<ArticleLocal>
Repository:
fun getArticleDetails(title: String): Flow<ArticleLocal> {
return articleDao.getArticleDetails(title)
}
ViewModel:
val articleDetail = MutableStateFlow<ArticleLocal>(ArticleLocal("","","","",""))
fun getArticle(title: String) {
viewModelScope.launch {
articleRepository.getArticleDetails(title).collect {
articleDetail.emit(it)
}
}
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title)
viewModel.articleDetail.collect {
Log.d(TAG, "onCreate: $it")
}
}
Problem with this code is that articleDetail on first touch gives me empty ArticleLocal e.g. title = "" I defined in ViewModel, later I get good result.
EDIT: With MyActivity .collet I get whole object but cannot access propert like it.title
Use a SharedFlow so it doesn't have to publish a default result. The flow won't emit anything until it receives its first value. Use replay = 1 to get similar behavior as StateFlow as far as new subscribers getting the most recent value immediately.
You also need to consider that if the title changes, it should not keep publishing values with the old title. Currently, you have it collecting from more and more flows each time the title changes.
If you use another MutableSharedFlow just for the title, you can get it to automatically cancel unnecessary collection of those old title flows. It also allows you to get the benefit of SharingStarted.WhileSubscribed to avoid unnecessary collection from the repository when there are no subscribers.
In ViewModel:
private val articleTitle = MutableSharedFlow<String>(bufferOverflow = BufferOverflow.DROP_OLDEST)
val articleDetail = articleTitle.flatMapLatest { articleRepository.getArticleDetails(it) }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
fun getArticle(title: String) {
articleTitle.tryEmit(title)
}
You can get rid of additional flow to emit data and use the flow returned from the repository directly.
ViewModel:
fun getArticle(title: String): Flow<ArticleLocal> {
return articleRepository.getArticleDetails(title)
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title).collect {
Log.d(TAG, "onCreate: $it")
}
}

Android - Retrieving in item from a Room database by its ID

I'm trying to retrieve a single Client item from my Room database. Every client is displayed in a list, and each client has an edit button on them. When the button is pressed, I would like to retrieve that client from the database by their id. Their details will then be displayed on an edit screen.
My problem arises in actually getting the client from the database. So far I have tried 2 approaches:
Coroutines based approach
I have tried to retrieve the item using coroutine based functions with Room. This approach does "work" to an extent, but in its current form the coroutine ends up retrieving the newly searched for client ***after*** the edit screen has been displayed. This makes it so that when you edit a client, you end up editing the one you tried to edit previously.
I have tried to counteract this by using .join(), using viewModelScope.async rather than launch and then attempting to use .await, and a few other ideas, but none of them have worked.
ClientDao.kt
#Dao
interface ClientDao {
#Query("SELECT * FROM tblClient WHERE id = :id")
suspend fun getClientToEdit(id: Int): List<Client>
}
ClientRepository.kt
class ClientRepository(private val clientDao: ClientDao) {
val clientSearchResults = MutableLiveData<List<Client>>()
suspend fun getClientToEdit(id: Int) {
clientSearchResults.value = clientDao.getClientToEdit(id)
}
}
ClientViewModel.kt
class ClientViewModel(application: Application): ViewModel() {
private val repository: ClientRepository
val clientSearchResults: MutableLiveData<List<Client>>
init {
val clientDB = ManagementDatabase.getDatabase(application)
val clientDao = clientDB.clientDao()
repository = ClientRepository(clientDao)
clientSearchResults = repository.clientSearchResults
}
fun getClientToEdit(clientId: Int) = viewModelScope.launch {
repository.getClientToEdit(clientId)
}
}
ManagementApp.kt
ClientScreen(
onEditClient = { id ->
clientViewModel.getClientToEdit(id)
val editClientList: List<Client>? = clientViewModel.clientSearchResults.value
//This looks awful but it works
// It just gets the client details of the selected client
if (editClientList != null) {
if (editClientList.firstOrNull() != null) {
selectedClient = editClientList[0]
If I could just find a way to make it so that clientViewModel.getClientToEdit(id) fully executed before running the rest of the code in ManagementApp.kt, it would work. The problem is I'm not sure how.
Flow based approach:
I didn't really think this approach would work, but it was worth a shot. I have tried to retrieve the item using a flow list, in the same way I have been retrieving the whole list.
ClientDao.kt
#Dao
interface ClientDao {
#Query("SELECT * FROM tblClient WHERE id = :id")
fun getClientToEdit(id: Int): Flow<List<Client>>
}
ClientRepository.kt
class ClientRepository(private val clientDao: ClientDao) {
fun getClientSearchResults(id: Int): Flow<List<Client>> =
clientDao.getClientToEdit(id)
}
ClientViewModel.kt
class ClientViewModel(application: Application): ViewModel() {
private val repository: ClientRepository
init {
val clientDB = ManagementDatabase.getDatabase(application)
val clientDao = clientDB.clientDao()
repository = ClientRepository(clientDao)
}
fun getClientToEdit(clientId: Int): LiveData<List<Client>> {
return repository.getClientSearchResults(id = clientId).asLiveData()
}
}
ManagementApp.kt
ClientScreen(
onEditClient = { id ->
val editClientList by clientViewModel.getClientToEdit(id).observeAsState(listOf())
//This looks awful but it works
// It just gets the client details of the selected client
if (editClientList != null) {
if (editClientList.firstOrNull() != null) {
selectedClient = editClientList[0]
The problem with this approach is that .observeAsState gives me the '#Composable invocations can only happen from the context of a #Composable function' error (Although the snippet of code above is actually within a #Composable function).
If anyone could provide some much needed help I would greatly appreciate it. I'm new to Android and have struggled with Room quite a bit, so my apologies if the code isn't really up to scratch. Thank you.
When the button is pressed, I would like to retrieve that client from the database by their id. Their details will then be displayed on an edit screen.
If by "edit screen" you mean proper screen with ViewModel to which you navigate using for example androidx.navigation, better approach would be to just pass the id to that new screen and do the loading in its ViewModel.
If I could just find a way to make it so that clientViewModel.getClientToEdit(id) fully executed before running the rest of the code in ManagementApp.kt
You can do that by making getClientToEdit suspend fun and then doing something like this:
val scope = rememberCoroutineScope()
ClientScreen(
onEditClient = { id ->
scope.launch {
clientViewModel.getClientToEdit(id)
// now getClientToEdit was executed
}
}
)
I would also suggest returning Client directly from the getClientToEdit, using LiveData for that is not necessary
Although the snippet of code above is actually within a #Composable function
It's not, you are trying to call it from onClick callback and onClick is not marked with #Composable, so you cannot call composable functions from there.
To sum it up:
If the result of your action is navigation to another screen, you can do one of these:
Pass just the id to that other screen and do the loading there, as I suggested.
Launch coroutine inside onEditClient callback, load the client and navigate from there as shown above.
Load the client in ViewModel, update some state there and navigate based on that state, something like:
// ViewModel
val actions = MutableSharedFlow<Action>()
fun editClient(id: Int) = viewModelScope.launch {
val client = repository.getClientToEdit(clientId)
actions.emit(NavigateToEditScreen(client))
}
// Screen
val action by clientViewModel.actions.collectAsState()
LaunchedEffect(action) {
if (action is NavigateToEditScreen) {
// do the navigation using action.client
}
}
ClientScreen(
onEditClient = { id ->
clientViewModel.editClient(id)
}
)

How to manage dependent Kotlin coroutines in Android

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
}
}
}

Android: await() seems not to work using Room database

I am working on an app to practise some calculations which saves each given answers and data about practise sessions into a Room database for tracking progress. There is a table that contains the answers and there is a table which contains the sessions, and each row in answer table needs to contain the id of the session in which the answer was given
I am using a Room database thus using coroutines when writing to database.
When the user clicks a button the answer is evaluated and saved, and also the session data is updated.
(Such as number of questions answered and the average score.)
To achieve this, I need to have the Id of the freshly created session data. So what I am trying is to call a method in the init{} block of the ViewModel which uses a Defferred and call await() to wait for the session data to be inserted and then get the last entry from the database and update the instance of SessionData that the view model holds and only when it is all done I enable the button thus we will not try to save any data before we know the current session id.
To test this out, I am using the Log.d() method to print out the current session id.
The problem is that I don't always get the right values. Sometimes I get the same id as previous session was, sometimes I get the correct one (so Logcat in Android Studio looks like: 33,33,35,36,38,38,40,41,42,...etc). However if I get all data from the database and check it out, all the ids are in the database, in correct order, no values are skipped.
For me it seems that await() doesn't actually make the app to wait, it seems to me that the reading of the database sometimes happenes before the writing is complete.
But I have no idea why.
In the ViewModel class:
SimpleConversionFragmentViewModel(
val conversionProperties: ConversionProperties,
val databaseDao: ConversionTaskDatabaseDao,
application: Application) : AndroidViewModel(application){
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private lateinit var sessionData : SessionData
...
init{
startNewSession()
}
...
/**
* This method starts and gets the current session
*/
private fun startNewSession() {
uiScope.launch {
/** Initialize session data*/
sessionData = SessionData()
sessionData.taskCategory = conversionProperties.taskCategory
sessionData.taskType = conversionProperties.taskType
/**
* First insert the new session wait for it to be inserted and get the session inserted
* because we need it's ID
**/
val createNewSession = async { saveSessionDataSuspend() }
val getCurrentSessionData = async { getCurrentSessionSuspend() }
var new = createNewSession.await()
sessionData = getCurrentSessionData.await() ?: SessionData()
_isButtonEnabled.value = true //Only let user click when session id is received!!
Log.d("Coroutine", "${sessionData.sessionId}")
}
}
/**
* The suspend function to get the current session
*/
private suspend fun getCurrentSessionSuspend() : SessionData? {
return withContext(Dispatchers.IO){
var data = databaseDao.getLastSession()
data
}
}
/**
* The suspend function for saving session data
*/
private suspend fun saveSessionDataSuspend() : Boolean{
return withContext(Dispatchers.IO) {
databaseDao.insertSession(sessionData)
true
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
And here is some details from the ConversionTaskDatabaseDao class:
#Dao
interface ConversionTaskDatabaseDao {
#Insert(entity = SessionData::class)
fun insertSession(session: SessionData)
#Query("SELECT * FROM session_data_table ORDER BY session_id DESC LIMIT 1")
fun getLastSession() : SessionData?
...
}
Has anyone got any idea how to solve this?
My first attempt was actually to save the session data only once in the onCleared() method of the ViewModel, but because I have to call the viewModelJob.cancel() method to prevent memory leaks, the job is cancelled before saving is done. But I think it would be a more efficient way if I could save the data here only once.
Or is there a better way to achive what I am trying to do?
Thanks in advance for any help,
Best regards: Agoston
My thought is since you need to wait one suspend method for another and they are already inside coroutine (initiated with launch-builder), you don't need await and you can simplify this:
val createNewSession = async { saveSessionDataSuspend() }
val getCurrentSessionData = async { getCurrentSessionSuspend() }
var new = createNewSession.await()
sessionData = getCurrentSessionData.await() ?: SessionData()
to that:
var new = saveSessionDataSuspend()
sessionData = getCurrentSessionSuspend()
On the contrary when you use await you have no guarantee what method would be first

What is the difference between emit and emitSource with LiveData ? ( as in REAL time USE-CASE )

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

Categories

Resources