I use MVVM architecture in my project. When it first called, it works. but after that, it does not work.
here is my code.
fun loadYgosuData() : LiveData<ArrayList<YgosuData>> {
Log.d(TAG, "loadYgosuData()...")
return liveData {
Log.d(TAG, "liveData Scope running....")
val element = withContext(Dispatchers.IO) {
Jsoup.connect("https://www.ygosu.com/community/real_article")
.get()
.select("div.board_wrap tbody tr")
}
emit(setYgosuData(element))
}
}
fun setYgosuData(tmpDoc: Elements?) : ArrayList<YgosuData> {
var tmpList = ArrayList<YgosuData>()
if (tmpDoc != null) {
//Log.d(TAG, "element : $tmpDoc")
for (e in tmpDoc) {
var title = e.select("td.tit a")
var name = e.select("td.name a")
var read = e.select("td.read")
var date = e.select("td.date")
var url = e.select("td.tit a").attr("href").toString()
var tmpYgosuData = YgosuData(
title = title.text(),
name = name.text(),
read = read.text(),
date = date.text(),
url = url
)
tmpList.add(tmpYgosuData)
}
}
Log.d(TAG, "여기2 ${tmpList.toString()}")
return tmpList
}
Could you help me please?
Thank you.
From official doc
The liveData building block serves as a structured concurrency primitive between coroutines and LiveData. The code block starts executing when LiveData becomes active and is automatically canceled after a configurable timeout when the LiveData becomes inactive. If it is canceled before completion, it is restarted if the LiveData becomes active again. If it completed successfully in a previous run, it doesn't restart. Note that it is restarted only if canceled automatically. If the block is canceled for any other reason (e.g. throwing a CancelationException), it is not restarted.
That's why it works for the first time as you mentioned.
Solution1: combine liveData with Transformations
private val body: MutableLiveData<String> = MutableLiveData()
val YgousData = body.switchMap {
liveData {
Log.d(TAG, "liveData Scope running....")
val element = withContext(Dispatchers.IO) {
Jsoup.connect("https://www.ygosu.com/community/real_article")
.get()
.select("div.board_wrap tbody tr")
}
emit(setYgosuData(element))
}
}
fun callApi(param:String){
body.value = param
}
You can change the body based on which you want to trigger api call
Solution2: You can do api call explicitly and then post the data to livedata (w/o using livedata builder)
Related
I have an observable in my foreground service which fetch data from a paging API and save it to the database, the foreground service shows a notification with a progress bar with number of saved items vs the total amount.
Observable which fetch all the data looks like this:
private fun getAllProducts(): Observable<Response<List<ProdottoBarcode>>> {
val lastId = intArrayOf(0)
return Observable.range(1, Integer.MAX_VALUE - 1)
.concatMap { currentPage -> getProducts(currentPage, lastId[0]) }
.takeUntil { response -> response.body()?.isEmpty() == true }
.doOnNext { response ->
lastId[0] = response.headers().get("lastId")?.toInt()!!
}
}
Then the subscription is done in onCreate() like this:
override fun onCreate() {
super.onCreate()
...
getAllProducts().subscribeWith(object: DisposableObserver<Response<List<ProdottoBarcode>>>() {
override fun onNext(response: Response<List<ProdottoBarcode>>) {
if (response.isSuccessful) {
val products = response.body()
val totalItems = response.headers().get("items")?.toInt()
insertProducts(totalItems, products)
}
}
override fun onError(e: Throwable) {
stopService()
}
override fun onComplete() {
}
})
}
And the method which saves all the data to the database looks like this:
private fun insertProducts(totalItems: Int?, products: List<ProdottoBarcode>?) {
if (products != null) {
CoroutineScope(Dispatchers.IO).launch {
for (product in products) {
repository.insert(product)
savedItems += 1
val notification =
totalItems?.let { items ->
NotificationCompat.Builder(baseContext, "progress_channel")
.setSmallIcon(R.drawable.ic_box)
.setContentTitle("Sincronizzati: $savedItems prodotti su $totalItems")
.setProgress(items, savedItems, false)
.setOngoing(true)
.build()
}
notificationManager.notify(notificationId, notification)
}
// TODO: stop the service and dismiss the notification when all items has been saved
if (savedItems == totalItems) {
stopService()
}
}
}
}
The stopService() in insertProducts not always works, while if I try to put stopService in onComplete() it will be executed once all subscriptions are done and NOT when all the items has been saved.
So my question is:
How can I stop my services by using the Coroutine inside the Observable? I need to know when all items from all observables are insert in database and only then to dismiss the service.
Side note: you don't need to do Int wrapping like this in Kotlin like you would in Java. Kotlin has implicit variable wrapping, so you can simply use a var local variable and it will be captured by whatever function you use it in.
val lastId = intArrayOf(0) // can just be var lastId = 0
Starting with getProducts() for fetching a page. I think the code you linked is OK provided your Retrofit service's getProducts function is marked suspend, so it's not blocking. No changes here.
private suspend fun getProducts(
page: Int,
lastId: Int,
itemsPerPage: Int = 50
): Response<List<ProdottoBarcode>> {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val urlServer = prefs.getString("server", "http://127.0.0.1/")!!
return RetrofitClient.getInstance().getService()
.getProducts(urlServer, "A", page, lastId, itemsPerPage)
}
Your getAllProducts in your linked code doesn't need backing StateFlows that are never collected--you're using them simply as mutable Int wrappers, which are unnecessary in Kotlin as mentioned way above. I'm not exactly sure how you're consuming these pages, since I'm not very familiar with Rx, but I take the use of concatMap to mean that the Observable is queuing up pages as fast as it can into a buffer, and you are reading out these pages to some local property that the UI uses. I think a buffer should be added so we can be inserting in the database in parallel with fetching the next page. Default buffer arguments are probably appropriate.
private val allProducts: Flow<Response<List<ProdottoBarcode>>> = flow {
var lastId = 0
for (currentPage in 1 until Int.MAX_VALUE) {
emit(getProducts(currentPage, lastId))
lastId = response.headers().get("lastId")!!.toInt()
}
}
.takeWhile { response -> !response.body().isNullOrEmpty() }
.buffer()
Usually, when you collect your flow, you should use an appropriate coroutine scope provided by the Android framework, so it will automatically cancel collection once it goes out of scope. If you inherit your service from LifecycleService, you can use the existing lifecycleScope. This is maybe not so critical in a service in this case, since I think you are only calling stopService() when your flow is complete, but it would make it a little more robust against potential mistakes, I think.
.launchIn is a shortcut that is like wrapping everything above it in launch and calling collect() on it. I prefer the syntax because it has less nesting of code.
override fun onCreate() {
super.onCreate()
// ...
allProducts.onEach { response ->
if (response.isSuccessful) {
val products = response.body()
val totalItems = response.headers().get("items")?.toInt()
insertProducts(totalItems, products)
}
}
.catch { Log.e(TAG, "Failed collecting page.", it) }
.onCompletion { stopService() }
.flowOn(Dispatchers.Default) // don't use main thread since this is a service
.launchIn(lifecycleScope)
}
Since we're using buffer() in the fetching flow, we don't need to launch other coroutines when inserting in the database to achieve parallelism. We can simplify this into a suspend function. We are handling stopping the service in the flow collector, so we don't need to do that here either. I'm assuming repository.insert is a suspend function, not blocking.
private suspend fun insertProducts(totalItems: Int?, products: List<ProdottoBarcode>?) {
if (totalItems == null) {
Log.e(TAG, "Tried to insert items without any item count. Skipping.")
return
}
if (products == null) {
Log.e(TAG, "Tried to insert null products list. Skipping.")
return
}
for (product in products) {
repository.insert(product)
savedItems += 1
val notification = NotificationCompat.Builder(baseContext, "progress_channel")
.setSmallIcon(R.drawable.ic_box)
.setContentTitle("Sincronizzati: $savedItems prodotti su $totalItems")
.setProgress(items, savedItems, false)
.setOngoing(true)
.build()
}
notificationManager.notify(notificationId, notification)
}
}
I saw this but I'm not sure how to implement it or if this is the same issue, I have a mediator live data that updates when either of its 2 source live datas update or when the underlying data (Room db) updates, it seems to work fine but if the data updates a lot it refreshes a lot in quick succession and I get an error
Cannot run invalidation tracker. Is the db closed?
Cannot access database on the main thread since it may potentially lock the UI for a long period of time
this doesn't happen everytime, only when there are a lot of updates to the database in very quick succession heres the problem part of the view model,
var search: MutableLiveData<String> = getSearchState()
val filters: MutableLiveData<MutableSet<String>> = getCurrentFiltersState()
val searchPokemon: LiveData<PagingData<PokemonWithTypesAndSpeciesForList>>
val isFiltersLayoutExpanded: MutableLiveData<Boolean> = getFiltersLayoutExpanded()
init {
val combinedValues =
MediatorLiveData<Pair<String?, MutableSet<String>?>?>().apply {
addSource(search) {
value = Pair(it, filters.value)
}
addSource(filters) {
value = Pair(search.value, it)
}
}
searchPokemon = Transformations.switchMap(combinedValues) { pair ->
val search = pair?.first
val filters = pair?.second
if (search != null && filters != null) {
searchAndFilterPokemonPager(search, filters.toList())
} else null
}.distinctUntilChanged()
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonPager(search: String, filters: List<String>): LiveData<PagingData<PokemonWithTypesAndSpeciesForList>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
maxSize = 60
)
) {
if (filters.isEmpty()){
searchPokemonForPaging(search)
} else {
searchAndFilterPokemonForPaging(search, filters)
}
}.liveData.cachedIn(viewModelScope)
}
#SuppressLint("DefaultLocale")
private fun getAllPokemonForPaging(): PagingSource<Int, PokemonWithTypesAndSpecies> {
return repository.getAllPokemonWithTypesAndSpeciesForPaging()
}
#SuppressLint("DefaultLocale")
private fun searchPokemonForPaging(search: String): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchPokemonWithTypesAndSpeciesForPaging(search)
}
#SuppressLint("DefaultLocale")
private fun searchAndFilterPokemonForPaging(search: String, filters: List<String>): PagingSource<Int, PokemonWithTypesAndSpeciesForList> {
return repository.searchAndFilterPokemonWithTypesAndSpeciesForPaging(search, filters)
}
the error is actually thrown from the function searchPokemonForPaging
for instance it happens when the app starts which does about 300 writes but if I force the calls off the main thread by making everything suspend and use runBlocking to return the Pager it does work and I don't get the error anymore but it obviously blocks the ui, so is there a way to maybe make the switchmap asynchronous or make the searchAndFilterPokemonPager method return a pager asynchronously? i know the second is technically possible (return from async) but maybe there is a way for coroutines to solve this
thanks for any help
You can simplify combining using combineTuple (which is available as a library that I wrote for this specific purpose) (optional)
Afterwards, you can use the liveData { coroutine builder to move execution to background thread
Now your code will look like
val search: MutableLiveData<String> = getSearchState()
val filters: MutableLiveData<Set<String>> = getCurrentFiltersState()
val searchPokemon: LiveData<PagingData<PokemonWithTypesAndSpeciesForList>>
val isFiltersLayoutExpanded: MutableLiveData<Boolean> = getFiltersLayoutExpanded()
init {
searchPokemon = combineTuple(search, filters).switchMap { (search, filters) ->
liveData {
val search = search ?: return#liveData
val filters = filters ?: return#liveData
withContext(Dispatchers.IO) {
emit(searchAndFilterPokemonPager(search, filters.toList()))
}
}
}.distinctUntilChanged()
}
What I'm trying to do
I have an app that's using Room with Coroutines to save search queries in the database. It's also possible to add search suggestions and later on I retrieve this data to show them on a list. I've also made it possible to "pin" some of those suggestions.
My data structure is something like this:
#Entity(
tableName = "SEARCH_HISTORY",
indices = [Index(value = ["text"], unique = true)]
)
data class Suggestion(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "suggestion_id")
val suggestionId: Long = 0L,
val text: String,
val type: SuggestionType,
#ColumnInfo(name = "insert_date")
val insertDate: Calendar
)
enum class SuggestionType(val value: Int) {
PINNED(0), HISTORY(1), SUGGESTION(2)
}
I have made the "text" field unique to avoid repeated suggestions with different states/types. E.g.: A suggestion that's a pinned item and a previously queried text.
My Coroutine setup looks like this:
private val parentJob: Job = Job()
private val IO: CoroutineContext
get() = parentJob + Dispatchers.IO
private val MAIN: CoroutineContext
get() = parentJob + Dispatchers.Main
private val COMPUTATION: CoroutineContext
get() = parentJob + Dispatchers.Default
And my DAOs are basically like this:
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(obj: Suggestion): Long
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(objList: List<Suggestion>): List<Long>
I also have the following public functions to insert the data into the database:
fun saveQueryToDb(query: String, insertDate: Calendar) {
if (query.isBlank()) {
return
}
val suggestion = Suggestion(
text = query,
insertDate = insertDate,
type = SuggestionType.HISTORY
)
CoroutineScope(IO).launch {
suggestionDAO.insert(suggestion)
}
}
fun addPin(pin: String) {
if (pin.isBlank()) {
return
}
val suggestion = Suggestion(
text = pin,
insertDate = Calendar.getInstance(),
type = SuggestionType.PINNED
)
CoroutineScope(IO).launch {
suggestionDAO.insert(suggestion)
}
}
fun addSuggestions(suggestions: List<String>) {
addItems(suggestions, SuggestionType.SUGGESTION)
}
private fun addItems(items: List<String>, suggestionType: SuggestionType) {
if (items.isEmpty()) {
return
}
CoroutineScope(COMPUTATION).launch {
val insertDate = Calendar.getInstance()
val filteredList = items.filterNot { it.isBlank() }
val suggestionList = filteredList.map { History(text = it, insertDate = insertDate, suggestionType = suggestionType) }
withContext(IO) {
suggestionDAO.insert(suggestionList)
}
}
}
There are also some other methods, but let's focus on the ones above.
EDIT: All of the methods above are part of a lib that I made, they're are not made suspend because I don't want to force a particular type of programming to the user, like forcing to use Rx or Coroutines when using the lib.
The problem
Let's say I try to add a list of suggestions using the addSuggestions() method stated above, and that I also try to add a pinned suggestion using the addPin() method. The pinned text is also present in the suggestion list.
val list = getSuggestions() // Getting a list somewhere
addSuggestions(list)
addPin(list.first())
When I try to do this, sometimes the pin is added first and then it's overwritten by the suggestion present in the list, which makes me think I might've been dealing with some sort of race condition. Since the addSuggestions() method has more data to handle, and both methods will run in parallel, I believe the addPin() method is completing first.
Now, my Coroutines knowledge is pretty limited and I'd like to know if there's a way to enqueue those method calls and make sure they'll execute in the exact same order I invoked them, that must be strongly guaranteed to avoid overriding data and getting funky results later on. How can I achieve such behavior?
I'd follow the Go language slogan "Don't communicate by sharing memory; share memory by communicating", that means instead of maintaining atomic variables or jobs and trying to synchronize between them, model your operations as messages and use Coroutines actors to handle them.
sealed class Message {
data AddSuggestions(val suggestions: List<String>) : Message()
data AddPin(val pin: String) : Message()
}
And in your class
private val parentScope = CoroutineScope(Job())
private val actor = parentScope.actor<Message>(Dispatchers.IO) {
for (msg in channel) {
when (msg) {
is Message.AddSuggestions -> TODO("Map to the Suggestion and do suggestionDAO.insert(suggestions)")
is Message.AddPin -> TODO("Map to the Pin and do suggestionDAO.insert(pin)")
}
}
}
fun addSuggestions(suggestions: List<String>) {
actor.offer(Message.AddSuggestions(suggestions))
}
fun addPin(pin: String) {
actor.offer(Message.AddPin(pin))
}
By using actors you'll be able to queue messages and they will be processed in FIFO order.
By default when you call .launch{}, it launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is canceled.
It doesn't care or wait for other parts of your code it just runs.
But you can pass a parameter to basically tell it to run immediately or wait for other Coroutine to finish(LAZY).
For Example:
val work_1 = CoroutineScope(IO).launch( start = CoroutineStart.LAZY ){
//do dome work
}
val work_2 = CoroutineScope(IO).launch( start = CoroutineStart.LAZY ){
//do dome work
work_1.join()
}
val work_3 = CoroutineScope(IO).launch( ) {
//do dome work
work_2.join()
}
When you execute the above code first work_3 will finish and invoke work_2 when inturn invoke Work_1 and so on,
The summary of coroutine start options is:
DEFAULT -- immediately schedules coroutine for execution according to its context
LAZY -- starts coroutine lazily, only when it is needed
ATOMIC -- atomically (in a non-cancellable way) schedules coroutine for execution according to its context
UNDISPATCHED -- immediately executes coroutine until its first suspension point in the current thread.
So by default when you call .launch{} start = CoroutineStart.DEFAULT is passed because it is default parameter.
Don't launch coroutines from your database or repository. Use suspending functions and then switch dispatchers like:
suspend fun addPin(pin: String) {
...
withContext(Dispatchers.IO) {
suggestionDAO.insert(suggestion)
}
}
Then from your ViewModel (or Activity/Fragment) make the call:
fun addSuggestionsAndPinFirst(suggestions: List<Suggestion>) {
myCoroutineScope.launch {
repository.addSuggestions(suggestions)
repository.addPin(suggestions.first())
}
}
Why do you have a separate addPin() function anyways? You can just modify a suggestion and then store it:
fun pinAndStoreSuggestion(suggestion: Suggestion) {
myCoroutineScope.launch {
repository.storeSuggestion(suggestion.copy(type = SuggestionType.PINNED)
}
}
Also be careful using a Job like that. If any coroutine fails all your coroutines will be cancelled. Use a SupervisorJob instead. Read more on that here.
Disclaimer: I do not approve of the solution below. I'd rather use an old-fashioned ExecutorService and submit() my Runnable's
So if you really want to synchronize your coroutines in a way that the first function called is also the first one to write to your database. (I'm not sure it is guaranteed since your DAO functions are also suspending and Room uses it's own threads too). Try something like the following unit test:
class TestCoroutineSynchronization {
private val jobId = AtomicInteger(0)
private val jobToRun = AtomicInteger(0)
private val jobMap = mutableMapOf<Int, () -> Unit>()
#Test
fun testCoroutines() = runBlocking {
first()
second()
delay(2000) // delay so our coroutines finish
}
private fun first() {
val jobId = jobId.getAndIncrement()
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
delay(1000) // intentionally delay your first coroutine
withContext(Dispatchers.IO) {
submitAndTryRunNextJob(jobId) { println(1) }
}
}
}
private fun second() {
val jobId = jobId.getAndIncrement()
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
submitAndTryRunNextJob(jobId) { println(2) }
}
}
private fun submitAndTryRunNextJob(jobId: Int, action: () -> Unit) {
synchronized(jobMap) {
jobMap[jobId] = action
tryRunNextJob()
}
}
private fun tryRunNextJob() {
var action = jobMap.remove(jobToRun.get())
while (action != null) {
action()
action = jobMap.remove(jobToRun.incrementAndGet())
}
}
}
So what I do on each call is increment a value (jobId) that is later used to prioritize what action to run first. Since you are using suspending function you probably need to add that modifier to the action submitted too (e.g. suspend () -> Unit).
I am trying to do an API call using coroutine and retrofit with the MVVM architecture. I would like to show a progress bar while waiting for the API response to be ready (with a timeout of 3 seconds).
In the View Model I am using Coroutine.LiveData
class BootstrapViewModel: ViewModel() {
private val repository : ConfigRepository =
ConfigRepository()
val configurations = liveData(Dispatchers.IO) {
val retrievedConfigs = repository.getConfigurations(4)
emit(retrievedConfigs)
}
}
What I have so far in the activity is just a simulation of API call to update progress bar:
launch {
// simulate API call
val configFetch = async(Dispatchers.IO) {
while (progressState.value != 100) {
progressState.postValue(progressState.value?.plus(1))
delay(50)
}
}
// suspend until fetch is finished or return null in 3 sec
val result = withTimeoutOrNull(3000) { configFetch.await() }
if (result != null) {
// todo: process config... next steps
} else {
// cancel configFetch
configFetch.cancel()
// show error
}
}
I can also observe the livedata as below and works fine:
bootstrapViewModel.configurations.observe(this, Observer {
//response is ready
})
Everything works fine separated. However, when I try to use the livedata inside coroutine scope things get messy. Is there anyway to await() for a coroutine livedata (like how I did for configFetch)?
You can do it like this:
val _progressBarVisibility = MutableLiveData<Int>() // Use this with postValue
val progressBarVisibility: LiveData<Int> = _progressBarVisibility
val configurations = liveData(Dispatchers.IO) {
_progressBarVisibility.postValue(View.VISIBLE)
// you can just stimulate API call with a delay() method
delay(3000) //3 seconds
val retrievedConfigs = repository.getConfigurations(4)
_progressBarVisibility.postValue(View.GONE)
emit(retrievedConfigs)
}
After that in your Activity:
viewModel.progressBarVisibility.observe(this, Observer{
pbVisibilityView.visibity = it
}
If you are asking about retrofit in particular, this is how you can do it.
In your DataApi interface, you just mark the method as suspended:
interface DataApi{
#GET("endpointHere")
suspend fun getData() : Result<Data>
}
The rest is just as I described above. Just replace getConfigurations(4) with getData()
I'm using LiveData's version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha05". Once my LiveData block executes successfully I want to explicitly trigger it to execute again, e.g.
I navigate to a fragment
User's data loads
I click delete btn while being in the same fragment
User's data should refresh
I have a fragment where I observe my LiveData, a ViewModel with LiveData and Repository:
ViewModel:
fun getUserLiveData() = liveData(Dispatchers.IO) {
val userData = usersRepo.getUser(userId)
emit(userData)
}
Fragment:
viewModel.getUserLiveData.observe(viewLifecycleOwner,
androidx.lifecycle.Observer {..
Then I'm trying to achieve desired behaviour like this:
viewModel.deleteUser()
viewModel.getUserLiveData()
According to the documentation below LiveData block won't execute if it has completed successfully and if I put a while(true) inside the LiveData block, then my data refreshes, however I don't want this to do since I need to update my view reactively.
If the [block] completes successfully or is cancelled due to reasons other than [LiveData]
becoming inactive, it will not be re-executed even after [LiveData] goes through active
inactive cycle.
Perhaps I'm missing something how I can reuse the same LiveDataScope to achieve this? Any help would be appreciated.
To do this with liveData { .. } block you need to define some source of commands and then subscribe to them in a block. Example:
MyViewModel() : ViewModel() {
val commandsChannel = Channel<Command>()
val liveData = livedata {
commandsChannel.consumeEach { command ->
// you could have different kind of commands
//or emit just Unit to notify, that refresh is needed
val newData = getSomeNewData()
emit(newData)
}
}
fun deleteUser() {
.... // delete user
commandsChannel.send(RefreshUsersListCommand)
}
}
Question you should ask yourself: Maybe it would be easier to use ordinary MutableLiveData instead, and mutate its value by yourself?
livedata { ... } builder works well, when you can collect some stream of data (like a Flow / Flowable from Room DB) and not so well for plain, non stream sources, which you need to ask for data by yourself.
I found a solution for this. We can use switchMap to call the LiveDataScope manually.
First, let see the official example for switchMap:
/**
* Here is an example class that holds a typed-in name of a user
* `String` (such as from an `EditText`) in a [MutableLiveData] and
* returns a `LiveData` containing a List of `User` objects for users that have
* that name. It populates that `LiveData` by requerying a repository-pattern object
* each time the typed name changes.
* <p>
* This `ViewModel` would permit the observing UI to update "live" as the user ID text
* changes.
**/
class UserViewModel: AndroidViewModel {
val nameQueryLiveData : MutableLiveData<String> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = nameQueryLiveData.switchMap {
name -> myDataSource.usersWithNameLiveData(name)
}
fun setNameQuery(val name: String) {
this.nameQueryLiveData.value = name;
}
}
The example was very clear. We just need to change nameQueryLiveData to your own type and then combine it with LiveDataScope. Such as:
class UserViewModel: AndroidViewModel {
val _action : MutableLiveData<NetworkAction> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = _action.switchMap {
action -> liveData(Dispatchers.IO){
when (action) {
Init -> {
// first network request or fragment reusing
// check cache or something you saved.
val cache = getCache()
if (cache == null) {
// real fecth data from network
cache = repo.loadData()
}
saveCache(cache)
emit(cache)
}
Reload -> {
val ret = repo.loadData()
saveCache(ret)
emit(ret)
}
}
}
}
// call this in activity, fragment or any view
fun fetchData(ac: NetworkAction) {
this._action.value = ac;
}
sealed class NetworkAction{
object Init:NetworkAction()
object Reload:NetworkAction()
}
}
First add implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file. Make your ViewModel as follows:
MyViewModel() : ViewModel() {
val userList = MutableLiveData<MutableList<User>>()
fun getUserList() {
viewModelScope.launch {
userList.postValue(usersRepo.getUser(userId))
}
}
}
Then onserve the userList:
viewModel.sessionChartData.observe(viewLifecycleOwner, Observer { users ->
// Do whatever you want with "users" data
})
Make an extension to delete single user from userList and get notified:
fun <T> MutableLiveData<MutableList<T>>.removeItemAt(index: Int) {
if (!this.value.isNullOrEmpty()) {
val oldValue = this.value
oldValue?.removeAt(index)
this.value = oldValue
} else {
this.value = mutableListOf()
}
}
Call that extension function to delete any user and you will be notified in your Observer block after one user get deleted.
viewModel.userList.removeItemAt(5) // Index 5
When you want to get userList from data source just call viewModel.getUserList() You will get data to the observer block.
private val usersLiveData = liveData(Dispatchers.IO) {
val retrievedUsers = MyApplication.moodle.getEnrolledUsersCoroutine(course)
repo.users = retrievedUsers
roles.postValue(repo.findRolesByAll())
emit(retrievedUsers)
}
init {
usersMediator.addSource(usersLiveData){ usersMediator.value = it }
}
fun refreshUsers() {
usersMediator.removeSource(usersLiveData)
usersMediator.addSource(usersLiveData) { usersMediator.value = it }
The commands in liveData block {} doesn't get executed again.
Okay yes, the observer in the viewmodel holding activity get's triggered, but with old data.
No further network call.
Sad. Very sad. "Solution" seemed promisingly and less boilerplaty compared to the other suggestions with Channel and SwitchMap mechanisms.
You can use MediatorLiveData for this.
The following is a gist of how you may be able to achieve this.
class YourViewModel : ViewModel() {
val mediatorLiveData = MediatorLiveData<String>()
private val liveData = liveData<String> { }
init {
mediatorLiveData.addSource(liveData){mediatorLiveData.value = it}
}
fun refresh() {
mediatorLiveData.removeSource(liveData)
mediatorLiveData.addSource(liveData) {mediatorLiveData.value = it}
}
}
Expose mediatorLiveData to your View and observe() the same, call refresh() when your user is deleted and the rest should work as is.