Android Kotlin Realm Proper Way to Query+Update Async - android

I recently faced a problem in which i had in memory a RealResult List of objects and was showing it in a view. Upon user click, the current showing item should be marked as deleted in realm (property isDeleted)
So i was just getting that object from the lazy RealmResults list, open a transaction and mark it deleted. As the RealmResults are auto-updated, i had a change listener bound to a notifityDataSetChanged. Everything works fine except this warning:
Mixing asynchronous queries with local writes should be avoided. Realm will convert any async queries to synchronous in order to remain consistent. Use asynchronous writes instead
Which is problematic because my list is enormous and i don't want the query to become sync. I solved it this way, which i don't know it it is right. Instead of giving the item object to the update function, i give the id of the object, and then do this:
Realm.getDefaultInstance().use { realm ->
realm.executeTransactionAsync {
// find the item
realm.where(ItemRealm::class.java)
.equalTo(ItemRealm.ID, itemId).findFirstAsync()
.addChangeListener(object : RealmChangeListener<ItemRealm> {
override fun onChange(element: ItemRealm) {
element.deleted = true
element.removeChangeListener(this)
}
})
}
}
The problem which im unsure is the async part of the query inside an async transaction.
Edit. Actually, it throws java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.
Edit2: Tried this and show Realm access on different thread:
fun setItemDeleted(itemId: Long) {
// write so new realm instance
Realm.getDefaultInstance().use { realm ->
realm.executeTransactionAsync {
// find the item
val item = realm.where(ItemRealm::class.java)
.equalTo(ItemRealm.TIMESTAMP, itemId).findFirst()
item?.userDeleted = true
}
}
}

Everything in executeTransactionAsync() runs on a background thread, so you should use the synchronous methods to obtain your object inside it.
Realm.getDefaultInstance().use { realm ->
realm.executeTransactionAsync { bgRealm ->
// find the item
val item = bgRealm.where(ItemRealm::class.java)
.equalTo(ItemRealm.ID, itemId).findFirst()
item?.deleted = true
}
}

Related

How to write Android ViewModel properly and move logic out of it?

I'm trying to use MVVM with ViewModel, ViewBinding, Retrofit2 in Android/Kotlin.
I don't know how to move application logic from ViewModel. I can't just move methods with logic because they run on viewModelScope and put results into observable objects in my ViewModel.
Or maybe I can?
For example I have some ArrayList (to show on some ListView).
// items ArrayList
private val _stocktakings =
MutableLiveData<ArrayList<InventoryStocktakingWithCountsDto?>>(ArrayList())
val stocktakings : LiveData<ArrayList<InventoryStocktakingWithCountsDto?>> get() =
_stocktakings
// selected item
private val _selected_stocktaking = MutableLiveData<Int>>
val selected_stocktaking : LiveData<Int> get() = _selected_stocktaking
And a function that is called from my fragment:
public fun loadStocktakings() {
viewModelScope.launch {
Log.d(TAG, "Load stocktakings requested")
clearError()
try {
with(ApiResponse(ApiAdapter.apiClient.findAllStocktakings())){
if (isSuccessful && body != null){
Log.d(TAG, "Load Stocktakings done")
setStocktakings(body)
} else {
val e = "Can't load stocktakings, API error: {${errorMessage}}"
Log.e(TAG, e)
HandleError("Can't load stocktakings, API error: {${e}}") // puts error message into val lastError MutableLiveData...
}
}
} catch (t : Throwable) {
Log.e(TAG, "Can't load stocktakings, connectivity error: ${t.message}")
HandleError("Can't load stocktakings, API error: {${e}}") // puts error message into val lastError MutableLiveData...
}
}
}
Now I want to add another function that changes some field in one of stocktakings. Maybe something like:
public fun setSelectedStocktakingComplete() {
stocktakings.value[selected_stocktaking.value].isComplete = true;
// call some API function... another 15 lines of code?
}
How to do it properly?
I feel I have read wrong tutorials... This will end with fat viewmodel cluttered with viewModelScope.launch, error handling and I can't imagine what will happen when I start adding data/form validation...
Here, some tip for that
Make sure the ViewModel is only responsible for holding and managing
UI-related data.
Avoid putting business logic in the ViewModel. Instead, encapsulate
it in separate classes, such as Repository or Interactor classes.
Use LiveData to observe data changes in the ViewModel and update the
UI accordingly.
Avoid making network or database calls in the ViewModel. Instead,
use the Repository pattern to manage data operations and provide the
data to the ViewModel through a LiveData or other observable object.
Make sure the ViewModel does not hold context references, such as
Activity or Fragment.
Use a ViewModel factory to provide dependencies to the ViewModel, if
necessary.
you can ensure that your ViewModel is simple, easy to test,
and scalable. It also makes it easier to maintain your codebase, as
the business logic is separated from the UI logic.
hope you understand

How to listen for changes in Realm with Kotlin Flow in a Repository

I'm trying to implement the same behavior of how Flow with Room Database in which it already listens to changes. I am pretty confused about how to use the RealmInstance.toflow() in which it returns a Flow<Realm>, however, I don't want that. If we compare it to Room Database, you can already specify what return type you it to be(ex. Flow<Entity>. Since Realm doesn't have any kind of DAOs, I am currently left with using
RealmInstance.addChangeListener{
realm->
//Handle DB Changes here
}
I don't know how to integrate the code above in my repository since you cant emit inside the addChangeListener because it needs a coroutine however,i don't want a solution of having to create a Global coroutine. I currently have this on my ItemRepository:
override suspend fun getItems(): Flow<Resource<List<Item>>> = flow{
RealmInstance.addChangeListener{
realm->
//Handle DB Changes here
//You cant emit() here since it needs a coroutine
}
}
The bottom line problem is: that I want to listen to changes in the realm in which my repository already returns Flow of the Object I want. Something like how Room DB and Flows work.
When you want to convert a callback listener like Realm changeListener to a Coroutine, you have two options :
One- Using CallBackFlow :
return callbackFlow<Location> {
RealmInstance.addChangeListener{
realm->
trySend(realm.toString()) // for example to return a string
}
addOnFailureListener { e ->
close(e)
}
awaitClose {
// remove and close your resources
}
}
TWO- Use suspendCancellableCoroutine
return suspendCancellableCoroutine { continuation ->
RealmInstance.addChangeListener{
realm->
//Handle DB Changes here
continuation.resume(returnValue)
}
continuation.invokeOnCancellation {
// do cleanup
}

Using StateFlow To Update List Adapter

I am trying to switch from LiveData to StateFlow in populating my ListAdapter.
I currently have a MutableLiveData<List<CustomClass>> that I am observing to update the list adapter as such:
viewModel.mutableLiveDataList.observe(viewLifecycleOwner, Observer {
networkIngredientAdapter.submitList(it)
}
This works fine. Now I am replacing the MutableLiveData<List<CustomClass>?> with MutableStateFlow<List<CustomClass>?> in the viewModel as such:
private val _networkResultStateFlow = MutableStateFlow<List<IngredientDataClass>?>(null)
val networkResultStateFlow : StateFlow<List<IngredientDataClass>?>
get() = _networkResultStateFlow
fun loadCustomClassListByNetwork() {
viewModelScope.launch {
//a network request using Retrofit
val result = myApi.myService.getItems()
_networkResultStateFlow.value = result
}
}
I am collecting the new list in a fragment as such:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
}
However, the list Adapter does not update when I call loadCustomClassListByNetwork(). Why am I not able to collect the value
Try to replace code in fragment with below:
lifecycleScope.launch {
viewModel.networkResultStateFlow.flowWithLifecycle(lifecycle)
.collect { }
}
I haven't mentioned in the original question but there are two collect calls being made in the created coroutine scope as such:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
viewModel.listOfSavedIngredients.collectLatest(){
list -> localIngredientAdapter.submitList(list)}
}
Previously only the first collect call was working so that only one list was updating. So I just created two separate coroutine scopes as such and it now works:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
}
lifecycleScope.launchWhenStarted {
viewModel.listOfSavedIngredients.collectLatest(){
list -> localIngredientAdapter.submitList(list)}
}
Note: Using launch, launchWhenStarted or launchWhenCreated yielded the same results.
I'll edit my response once I figure out the reason for needing separate scopes for each call to collect.
EDIT:
So the reason only one listAdapter was updating was because I needed a separate CoroutineScope for each of my StateFlow since Flows by definition run on coroutines. Each flow uses its respective coroutine scope to collect its own value and so you cannot have flows share the same coroutine scope b/c then they would be redundantly collecting the same value. The answer provided by #ruby6221 also creates a new coroutine scope and so likely works but I cannot test it due to an unrelated issue with upgrading my SDK version, otherwise I would set it as the correct answer.

Android: collecting a Kotlin Flow inside another not emitting

I have got the following method:
operator fun invoke(query: String): Flow<MutableList<JobDomainModel>> = flow {
val jobDomainModelList = mutableListOf<JobDomainModel>()
jobListingRepository.searchJobs(sanitizeSearchQuery(query))
.collect { jobEntityList: List<JobEntity> ->
for (jobEntity in jobEntityList) {
categoriesRepository.getCategoryById(jobEntity.categoryId)
.collect { categoryEntity ->
if (categoryEntity.categoryId == jobEntity.categoryId) {
jobDomainModelList.add(jobEntity.toDomainModel(categoryEntity))
}
}
}
emit(jobDomainModelList)
}
}
It searches in a repository calling the search method that returns a Flow<List<JobEntity>>. Then for every JobEntity in the flow, I need to fetch from the DB the category to which that job belongs. Once I have that category and the job, I can convert the job to a domain model object (JobDomainModel) and add it to a list, which will be returned in a flow as the return object of the method.
The problem I'm having is that nothing is ever emitted. I'm not sure if I'm missing something from working with flows in Kotlin, but I don't fetch the category by ID (categoriesRepository.getCategoryById(jobEntity.categoryId)) it then works fine and the list is emitted.
Thanks a lot in advance!
I think the problem is that you're collecting infinite length Flows, so collect never returns. You should use .take(1) to get a finite Flow before collecting it, or use first().
The Flows returned by your DAO are infinite length. The first value is the first query made, but the Flow will continue forever until cancelled. Each item in the Flow is a new query made when the contents of the database change.
Something like this:
operator fun invoke(query: String): Flow<MutableList<JobDomainModel>> =
jobListingRepository.searchJobs(sanitizeSearchQuery(query))
.map { jobEntityList: List<JobEntity> ->
jobEntityList.mapNotNull { jobEntity ->
categoriesRepository.getCategoryById(jobEntity.categoryId)
.first()
.takeIf { it.categoryId == jobEntity.categoryId }
}
}
Alternatively, in your DAO you could make a suspend function version of getCategoryById() that simply returns the list.
Get an idea from the code below if your Kotlin coroutine flow gets lost with a continuation approximate peak alloc exception
fun test(obj1: Object,obj2: Object) = flow {
emit(if (obj1 != null) repository.postObj(obj1).first() else IgnoreObjResponse)
}.map { Pair(it, repository.postObj(obj2).first()) }

Android Kotlin Realm Proper Way of Query+ Return Unmanaged Items on Bg Thread

What is the proper way of querying and returning an unmanaged result of items with realm, everything in the background thread?. I'm using somethibf like this:
return Observable.just(1)
.subscribeOn(Schedulers.io())
.map {
val realm = Realm.getDefaultInstance()
val results = realm.where(ItemRealm::class.java)
.equalTo("sent", false).findAll()
realm to results
}
.map {
val (realm, results) = it
val unManagedResults = realm.copyFromRealm(results)
realm.close()
unManagedResults
}
}
And then chaining this observable with another one that will post the results to a server.
The solution working, although is a bit ugly on this aspects:
No proper way of wrapping the realmQuery in an observable, because
there is no way of opening a realInstance in a background thread without this kind of cheat (at least that i know about), so i need to use this fake
observable Observable.just(1).
Not the best place to open and close Realm instances, inside first and second map
I don't know if it is guaranteed that the realm instance is closed after all the items have been copied.
So what is the proper way of Query and Return unmanaged results on the background thread (some context, i need this to send the results to a server, in the background and as this task is totally independent from my app current data flow, so it should be off the main thread).
Suggested Version:
return Observable.fromCallable {
Realm.getDefaultInstance().use { realm ->
realm.copyFromRealm(
realm.where(ItemRealm::class.java)
.equalTo(ItemRealm.FIELD_SEND, false).findAll()
)
}
}
This is how you would turn your Realm objects unmanaged:
return Observable.defer(() -> {
try(Realm realm = Realm.getDefaultInstance()) {
return Observable.just(
realm.copyFromRealm(
realm.where(ItemRealm.class).equalTo("sent", false).findAll()
)
);
}
}).subscribeOn(Schedulers.io());
Although this answer is Java, the Kotlin answer is just half step away.

Categories

Resources