By using LiveData's latest version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03", I have developed a code for a feature called "Search Products" in the ViewModel using LiveData's new building block (LiveData + Coroutine) that performs a synchronous network call using Retrofit and update different flags (isLoading, isError) in ViewModel accordingly. I am using Transforamtions.switchMap on "query" LiveData so whenever there is a change in "query" from the UI, the "Search Products" code starts its executing using Transformations.switchMap. Every thing is working fine, except that i want to cancel the previous Retrofit Call whenever a change happens in "query" LiveData. Currently i can't see any way to do this. Any help would be appreciated.
class ProductSearchViewModel : ViewModel() {
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
// Query Observable Field
val query: MutableLiveData<String> = MutableLiveData()
// IsLoading Observable Field
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val products: LiveData<List<ProductModel>> = query.switchMap { q ->
liveData(context = coroutineScope.coroutineContext) {
emit(emptyList())
_isLoading.postValue(true)
val service = MyApplication.getRetrofitService()
val response = service?.searchProducts(q)
if (response != null && response.isSuccessful && response.body() != null) {
_isLoading.postValue(false)
val body = response.body()
if (body != null && body.results != null) {
emit(body.results)
}
} else {
_isLoading.postValue(false)
}
}
}
}
You can solve this problem in two ways:
Method # 1 ( Easy Method )
Just like Mel has explained in his answer, you can keep a referece to the job instance outside of switchMap and cancel instantance of that job right before returning your new liveData in switchMap.
class ProductSearchViewModel : ViewModel() {
// Job instance
private var job = Job()
val products = Transformations.switchMap(_query) {
job.cancel() // Cancel this job instance before returning liveData for new query
job = Job() // Create new one and assign to that same variable
// Pass that instance to CoroutineScope so that it can be cancelled for next query
liveData(CoroutineScope(job + Dispatchers.IO).coroutineContext) {
// Your code here
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
Method # 2 ( Not so clean but self contained and reusable)
Since liveData {} builder block runs inside a coroutine scope, you can use a combination of CompletableDeffered and coroutine launch builder to suspend that liveData block and observe query liveData manually to launch jobs for network requests.
class ProductSearchViewModel : ViewModel() {
private val _query = MutableLiveData<String>()
val products: LiveData<List<String>> = liveData {
var job: Job? = null // Job instance to keep reference of last job
// LiveData observer for query
val queryObserver = Observer<String> {
job?.cancel() // Cancel job before launching new coroutine
job = GlobalScope.launch {
// Your code here
}
}
// Observe query liveData here manually
_query.observeForever(queryObserver)
try {
// Create CompletableDeffered instance and call await.
// Calling await will suspend this current block
// from executing anything further from here
CompletableDeferred<Unit>().await()
} finally {
// Since we have called await on CompletableDeffered above,
// this will cause an Exception on this liveData when onDestory
// event is called on a lifeCycle . By wrapping it in
// try/finally we can use this to know when that will happen and
// cleanup to avoid any leaks.
job?.cancel()
_query.removeObserver(queryObserver)
}
}
}
You can download and test run both of these methods in this demo project
Edit: Updated Method # 1 to add job cancellation on onCleared method as pointed out by yasir in comments.
Retrofit request should be cancelled when parent scope is cancelled.
class ProductSearchViewModel : ViewModel() {
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
/**
* Adding job that will be used to cancel liveData builder.
* Be wary - after cancelling, it'll return a new one like:
*
* ongoingRequestJob.cancel() // Cancelled
* ongoingRequestJob.isActive // Will return true because getter created a new one
*/
var ongoingRequestJob = Job(coroutineScope.coroutineContext[Job])
get() = if (field.isActive) field else Job(coroutineScope.coroutineContext[Job])
// Query Observable Field
val query: MutableLiveData<String> = MutableLiveData()
// IsLoading Observable Field
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val products: LiveData<List<ProductModel>> = query.switchMap { q ->
liveData(context = ongoingRequestJob) {
emit(emptyList())
_isLoading.postValue(true)
val service = MyApplication.getRetrofitService()
val response = service?.searchProducts(q)
if (response != null && response.isSuccessful && response.body() != null) {
_isLoading.postValue(false)
val body = response.body()
if (body != null && body.results != null) {
emit(body.results)
}
} else {
_isLoading.postValue(false)
}
}
}
}
Then you need to cancel ongoingRequestJob when you need to. Next time liveData(context = ongoingRequestJob) is triggered, since it'll return a new job, it should run without problems. All you need to left is cancel it where you need to, i.e. in query.switchMap function scope.
Related
I have a problem with Room that return LiveData.
I create Dao with function to returns list of data. I suppose to return as LiveData. But, it doesn't work as expected.
Dao function
#Transaction
#Query("SELECT * FROM AllocationPercentage WHERE id IN (:ids)")
fun getByIds(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>>
Here is how I observe it inside the ViewModel:
class AllocationViewModel(
private val getAllocationByIdUseCase: GetAllocationByIdUseCase,
private val getDetailByIdUseCase: GetAllocationPercentageByIdUseCase
) : ViewModel() {
var allocationUiState: LiveData<AllocationUiState> = MutableLiveData()
private set
var allocationPercentageUiState: LiveData<List<AllocationPercentageUiState>> = MutableLiveData()
private set
val mediatorLiveData = MediatorLiveData<List<AllocationPercentageUiState>>()
fun getAllocationById(allocationId: Long) = viewModelScope.launch(Dispatchers.IO) {
val result = getAllocationByIdUseCase(allocationId) // LiveData
allocationUiState = Transformations.map(result) {
AllocationUiState(allocation = it.allocation)
}
mediatorLiveData.addSource(result) { allocation ->
Log.d(TAG, "> getAllocationById")
val ids = allocation.percentages.map { percentage -> percentage.id }
val detailResult: LiveData<List<AllocationPercentageWithDetails>> =
getDetailByIdUseCase(ids) // LiveData
allocationPercentageUiState = Transformations.map(detailResult) { details ->
Log.d(TAG, ">> Transform : $details")
details.map {
AllocationPercentageUiState(
id = it.allocationPercentage.id,
percentage = it.allocationPercentage.percentage,
description = it.allocationPercentage.description,
currentProgress = it.allocationPercentage.currentProgress
)
}
}
}
}
}
The allocationPercentageUiState is observed by Fragment.
Log.d(TAG, "observeViewModel: ${it?.size}")
val percentages = it ?: return#observe
setAllocationPercentages(percentages) // update UI
}
allocationViewModel.mediatorLiveData.observe(viewLifecycleOwner) {}
And getDetailByIdUseCase just a function which directly return result from Dao.
class GetAllocationPercentageByIdUseCase(private val repository: AllocationPercentageRepository) {
operator fun invoke(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>> {
return repository.getAllocationPercentageByIds(ids)
}
}
Any idea why? Thank you.
Combining var with LiveData or MutableLiveData doesn't make sense. It defeats the purpose of using LiveData. If something comes along and observes the original LiveData that you have in that property, it will never receive anything. It will have no way of knowing there's a new LiveData instance it should be observing instead.
I can't exactly tell you how to fix it because your code above is incomplete, so I can't tell what you're trying to do in your mapping function, or whether it is called in some function vs. during ViewModel initialization.
I just want to know if it is possible for me to return activePodcastViewData. I get return not allow here anytime I tried to call it on the activePodcastViewData.Without the GlobalScope I do get everything working fine.However I updated my repository by adding suspend method to it.Hence I was getting Suspend function should only be called from a coroutine or another suspend function.
fun getPodcast(podcastSummaryViewData: PodcastViewModel.PodcastSummaryViewData): PodcastViewData? {
val repo = podcastRepo ?: return null
val url = podcastSummaryViewData.url ?: return null
GlobalScope.launch {
val podcast = repo.getPodcast(url)
withContext(Dispatchers.Main) {
podcast?.let {
it.feedTitle = podcastViewData.name ?: ""
it.imageUrl = podcastViewData.imageUrl ?: ""
activePodcastViewData = PodcastView(it)
activePodcastViewData
}
}
}
return null
}
class PodcastRepo {
val rssFeedService =RssFeedService.instance
suspend fun getPodcast(url:String):Podcast?{
rssFeedService.getFeed(url)
return Podcast(url,"No name","No Desc","No image")
}
I'm not sure that I understand you correctly but if you want to get activePodcastViewData from coroutine scope you should use some observable data holder. I will show you a simple example with LiveData.
At first, add implementation:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
Now, in your ViewModel we need to create mutableLiveData to hold and emit our future data.
val podcastsLiveData by lazy { MutableLiveData<Podcast>() }
Here your method: (I wouldn't recommend GlobalScope, let's replace it)
fun getPodcast(podcastSummaryViewData: PodcastViewModel.PodcastSummaryViewData): PodcastViewData? {
val repo = podcastRepo ?: return null
val url = podcastSummaryViewData.url ?: return null
CoroutineScope(Dispatchers.IO).launch {
val podcast = repo.getPodcast(url)
withContext(Dispatchers.Main) {
podcast?.let {
it.feedTitle = podcastViewData.name ?: ""
it.imageUrl = podcastViewData.imageUrl ?: ""
activePodcastViewData = PodcastView(it)
}
}
}
podcastsLiveData.postValue(activePodcastViewData)
}
As you can see your return null is turned to postValue(). Now you finally can observe this from your Activity:
viewModel.podcastsLiveData.observe(this) {
val podcast = it
//Use your data
}
viewModel.getPodcast()
Now every time you call viewModel.getPodcast() method, code in observe will be invoked.
I hope that I helped some :D
I have a method in my viewmodel that resets rows in the database.
fun resetScores() {
viewModelScope.launch {
for(player in players){
player.level = 1
player.score = 0
playerDao.updatePlayer(player) // updates the DB
}
}
}
var players = mutableListOf<Player>() -- > players is a mutable list
I have this unit test for testing this method
#Test
fun testResetScores() {
val context = ApplicationProvider.getApplicationContext<Context>()
val viewModel = PlayerViewModel(Phase10DataBase.getDatabase(context).playerDao)
viewModel.players = mutableListOf(Player(1,"Player1",5,100),
Player(2,"Player2",5,100),
Player(3,"Player3",5,100))
assertEquals(viewModel.players.get(1).score, 100)
viewModel.resetScores()
assertEquals(viewModel.players.get(1).score, 100)
}
And this test pass whereas I expect it to fail.
Looks like the test is not waiting for the async DB operation to be finished before calling the assert.
What is the right way to test it or should the actual code resetScore need to be updated?
Use runBlocking it blocks until the coroutine is finished
#Test
fun testResetScores() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val viewModel = PlayerViewModel(Phase10DataBase.getDatabase(context).playerDao)
viewModel.players = mutableListOf(Player(1,"Player1",5,100),
Player(2,"Player2",5,100),
Player(3,"Player3",5,100))
assertEquals(viewModel.players.get(1).score, 100)
viewModel.resetScores()
assertEquals(viewModel.players.get(1).score, 100)
}
I'm making a crawling logic by using coroutines in Kotlin but i don't know this code is right.
this is model class
suspend fun parseYgosu() : Elements? {
var data:Elements? = null
var x : Deferred<Elements?> = CoroutineScope(Dispatchers.IO).async {
var doc = Jsoup.connect("https://www.ygosu.com/community/real_article").get()
data = doc.select("div.board_wrap tbody tr")
data
}
x.await()
Log.d(TAG, "$data")
return data
}
This code have problems. I do not want it be a suspend function.
And also I want to get data from this function by calling it from repository class.
could you help me?
You can use liveData builder
fun parseYgosu(): LiveData<Elements?> = liveData {
val element = withContext(Dispatchers.IO) {
Jsoup.connect("https://www.ygosu.com/community/real_article")
.get()
.select("div.board_wrap tbody tr")
}
emit(element)
}
and UI side:
// for fragment
viewModel.parseYgosu().observe(viewLifecycleOwner, Observer { element -> ... })
// or for activity
viewModel.parseYgosu().observe(this, Observer { element -> ... })
Sticking with the future Deferred, if you don't want it to be suspend then you can't have await() in it
// Not suspend
fun parseYgosuAsync() = CoroutineScope(Dispatchers.IO).async {
val doc = Jsoup.connect("https://www.ygosu.com/community/real_article").get()
val data = doc.select("div.board_wrap tbody tr")
Log.d(TAG, "$data")
data
}
I'm using nested Coroutine blocks in my code. And I'm getting a null value when I tried to get Deferred type's result to a variable. Thus, It causes a casting problem which is kotlin.TypeCastException: null cannot be cast to non-null type kotlin.collections.ArrayList in getNearbyHealthInstitutions() method's return line. I believe, I did the right implementation at some point but what am I missing to get null value from Deferred's result? The funny thing is when I debug it, it does return the expected value. I think it should be the concurrency problem or I don't have any idea why it works in debug mode in the first place. Any ideas fellas?
// Invocation point where resides in a callback
GlobalScope.launch(Dispatchers.Main) {
nearbyHealthInstitutionSites.value = getNearbyHealthInstitutions()
}
private suspend fun getNearbyHealthInstitutions(radius: Meter = DEFAULT_KM_RADIUS) : ArrayList<Hospital> {
return CoroutineScope(Dispatchers.IO).async {
val list = getHealthInstitutions()
val filteredList = list?.filter { it.city == state?.toUpperCase() } as MutableList<Hospital>
Log.i(MTAG, "nearby list is $filteredList")
Log.i(MTAG, "nearby list's size is ${filteredList.size}")
var deferred: Deferred<MutableList<Hospital>>? = null
addAllNearbyLocations(onEnd = { nearbyHealthInstitutions ->
deferred = async {
findNearbyOfficialHealthInstitutions(
officialHealthInstitutionList = filteredList as ArrayList<Hospital>,
nearbyHealthInstitutions = nearbyHealthInstitutions
)
}
})
val result = deferred?.await()
return#async result as ArrayList<Hospital>
}.await()
}
private suspend fun findNearbyOfficialHealthInstitutions(officialHealthInstitutionList: ArrayList<Hospital>, nearbyHealthInstitutions: MutableList<Hospital>): MutableList<Hospital> {
return GlobalScope.async(Dispatchers.Default) {
val result = mutableListOf<Hospital>()
officialHealthInstitutionList.forEach {
nearbyHealthInstitutions.forEach { hospital ->
StringSimilarity.printSimilarity(it.name, hospital.name)
val similarity = StringSimilarity.similarity(it.name, hospital.name.toUpperCase())
if (similarity > SIMILARITY_THRESHOLD) {
Log.i(MTAG, "findNearbyOfficialHealthInstitutions() - ${it.name} and ${hospital.name.toUpperCase()} have %$similarity")
result.add(hospital)
}
}
}
Log.i(TAG, "------------------------------------------")
result.forEach {
Log.i(MTAG, "findNearbyOfficialHealthInstitutions() - hospital.name is ${it.name}")
}
return#async result
}.await()
}
Since addAllNearbyLocations() is asynchonous, your coroutine needs to wait for the callback to be called to continue its execution. You can use suspendCoroutine API for this.
val result = suspendCoroutine { continuation ->
addAllNearbyLocations(onEnd = { nearbyHealthInstitutions ->
findNearbyOfficialHealthInstitutions(
officialHealthInstitutionList = filteredList as ArrayList<Hospital>,
nearbyHealthInstitutions = nearbyHealthInstitutions
).let { found -> continuation.resume(found) }
})
}
On a separate note you should use List instead of ArrayList or MutableList, you should always look to use a generic interface instead of a specific implementation of that interface. This also gets rids of some of the castings (ideally you should have no castings in this code).