LiveData with two sources? - android

I'd like to use the same LiveData with different sources. One from an API call which is an observable and one from a database which is a LiveData. I'd like to be able to do something like this:
private LiveData<List<Items>> items = new MutableLiveData<>();
// this one comes from an API and it's an observable
public void onApiItemsSelected(String name) {
Disposable disposable = repository.getItemsFromApi(name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(itemsList -> {
items.setValue(itemsList);
});
compositeDisposable.add(disposable);
}
// this one comes from the database and it's a livedata that I want to transform first
public void onDatabaseItemsSelected() {
items = Transformations.map(repository.getItemsFromdatabase(), itemsList -> {
List<Items> finalItemsList = new ArrayList<>();
for (Item item : itemsList) {
finalItemsList.add(itemsList.toSomething());
}
return finalItemsList;
});
}
The problem is that a Transformation.map always returns a LiveData and in order to make a items.setValue(itemsList) items need to be a MutableLiveData. I tried with MediatorLiveData but it's calling the two sources and mixing everything. That's not what I need here. I need one source OR the other. Is it possible?

I think you could delegate this responsibility to your repository.
See for example this open source project and related article with more info.
Basically, the repository handles all the complexity of deciding which source to get the data from (see the JobRepository). It exposes a rx Observable to the ViewModel (see the JobsViewModel). Then all the ViewModel has to do is update the LiveData:
private val presentation: MutableLiveData<Presentation> = MutableLiveData()
fun bind() {
jobRepository.getJobs(true)
.subscribeOn(Schedulers.io())
.subscribe { resource: Resource<List<JobWithRelations>> ->
when (resource) {
// commenting some parts for brevity ...
is Resource.ResourceFound -> {
presentation.postValue(Presentation(getApplication(), resource.data!!))
}
// ...
}
}.addTo(compositeDisposable)
}
Managing multiple sources of data is complex. Among the many aspects involved in architecting this, things to keep in mind include:
In-memory caching expiration strategies
Policies for db caching, for example, when users are offline
Throttling and retry of API calls
Policies for updating the in-memory cache or db when new data comes from the network
One library that could help you is the Dropbox Store. With it, you could build your data sources like in the example below from their documentation:
StoreBuilder
.from(
fetcher = nonFlowValueFetcher { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
sourceOfTruth = SourceOfTrue.from(
reader = db.postDao()::loadPosts,
writer = db.postDao()::insertPosts,
delete = db.postDao()::clearFeed,
deleteAll = db.postDao()::clearAllFeeds
)
).build()
Then to get the data in your ViewModel:
private val presentation: MutableLiveData<Presentation> = MutableLiveData()
lifecycleScope.launchWhenStarted {
store.stream(StoreRequest.cached(key = key, refresh=true)).collect { response ->
when(response) {
// commenting some parts for brevity ...
is StoreResponse.Data -> {
presentation.postValue(response.value)
}
// ...
}
}
}

I am not sure it is the best practice but I think will do the trick.
public void onDatabaseItemsSelected() {
items.value = repository.getItemsFromdatabase().value;
};
The syntax may not be accurate as I am know little about java, I do mainly kotlin. But the thing is: when the user clicks Api, make your onApiItemsSelected() run, that will set the value of items, when the user clicks database, make onDatabaseItemsSelected() run, so it will replace items value for the result of repository.getItemsFromDatabase()

Related

How to clear repository cache when the user logs out?

I have an repository that contains an in-memory cache list inside a StateFlow. The problem is that whenever the user logs out and logs into another account, the old data from the previous user is still there.
object Repository {
private lateinit var remoteDataSource: RemoteDataSource
operator fun invoke(remoteDataSource: remoteDataSource) {
this.remoteDataSource = remoteDataSource
return this
}
private val myList = MutableStateFlow(listOf<myData>())
suspend fun getData(): Flow<List<myData>> =
withContext(Dispatchers.IO) {
if (myList.value.isEmpty()) {
val response = remoteDataSource.getData()
if (response != null) {
myList.value = response.map { it.toMyData() }
}
}
myList
}
suspend fun addData(newData: MyData) =
withContext(Dispatchers.IO) {
myList.value = myList.value.plus(newData)
remoteDataSource.addData(myData.toMyDataRequest())
}
}
This repository is used by multiple ViewModels. The list itself is only observed by one screen (let's call it myFragment), but other screens can add new elements to it. I've tried to clear the repository on myFragment's onDestroyView, but it clears the list whenever the user navigates away from myFragment (even when it's not a logout).
We could observe whenever the user logs out in an userRepository, but i don't know how to observe data in one repository from another repository (there's nothing like viewModelScope.launch to collect flows or something like that).
What approach can be used to solve this? And how would it clear the list?
i don't know how to observe data in one repository from another repository
I'd argue you shouldn't in this case.
You have a use-case: Logout.
When you invoke this use-case, you should perform al the necessary operations that your app requires. In this case, you should call your repository to let it know.
suspend fun clearData() =
withContext(Dispatchers.IO) {
// clear your data
}
I'd argue that you shouldn't hardcode the Dispatcher, since you'll likely test this at some point; in your tests you're going to use TestDispatcher or similar, and if you hardcode it, it will be harder to test. You write tests, right?
So now your use case..
class LogoutUseCase(repo: YourRepo) {
operator fun invoke() {
repo.clearData()
//do the logout
}
}
That's how I would think about this.
Your scope for all this is the UI that initiated the logout...

How to combine livedata and kotlin flow

Is this good to put the collect latest inside observe?
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!)
.observe(viewLifecycleOwner) {
if (it != null) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.referralDetailsResponse.collect { referralResponseState ->
when (referralResponseState) {
State.Empty -> {
}
is State.Failed -> {
Timber.e("${referralResponseState.message}")
}
State.Loading -> {
Timber.i("LOADING")
}
is State.Success<*> -> {
// ACCESS LIVEDATA RESULT HERE??
}}}}
I'm sure it isn't, my API is called thrice too as the local DB changes, what is the right way to do this?
My ViewModel looks like this where I'm getting user information from local Room DB and referral details response is the API response
private val _referralDetailsResponse = Channel<State>(Channel.BUFFERED)
val referralDetailsResponse = _referralDetailsResponse.receiveAsFlow()
init {
val inviteSlug: String? = savedStateHandle["inviteSlug"]
// Fire invite link
if (inviteSlug != null) {
referralDetail(inviteSlug)
}
}
fun referralDetail(referral: String?) = viewModelScope.launch {
_referralDetailsResponse.send(State.Loading)
when (
val response =
groupsRepositoryImpl.referralDetails(referral)
) {
is ResultWrapper.GenericError -> {
_referralDetailsResponse.send(State.Failed(response.error?.error))
}
ResultWrapper.NetworkError -> {
_referralDetailsResponse.send(State.Failed("Network Error"))
}
is ResultWrapper.Success<*> -> {
_referralDetailsResponse.send(State.Success(response.value))
}
}
}
fun fetchUserProfileLocal(username: String) =
userRepository.getUserLocal(username).asLiveData()
You can combine both streams of data into one stream and use their results. For example we can convert LiveData to Flow, using LiveData.asFlow() extension function, and combine both flows:
combine(
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!).asFlow(),
viewModel.referralDetailsResponse
) { userProfile, referralResponseState ->
...
}.launchIn(viewLifecycleOwner.lifecycleScope)
But it is better to move combining logic to ViewModel class and observe the overall result.
Dependency to use LiveData.asFlow() extension function:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
it certainly is not a good practice to put a collect inside the observe.
I think what you should do is collect your livedata/flows inside your viewmodel and expose the 'state' of your UI from it with different values or a combined state object using either Flows or Livedata
for example in your first code block I would change it like this
get rid of "userProfile" from your viewmodel
create and expose from your viewmodel to your activity three LiveData/StateFlow objects for your communityFeedPageData, errorMessage, refreshingState
then in your viewmodel, where you would update the "userProfile" update the three new state objects instead
this way you will take the business logic of "what to do in each state" outside from your activity and inside your viewmodel, and your Activity's job will become to only update your UI based on values from your viewmodel
For the specific case of your errorMessage and because you want to show it only once and not re-show it on Activity rotation, consider exposing a hot flow like this:
private val errorMessageChannel = Channel<CharSequence>()
val errorMessageFlow = errorMessageChannel.receiveAsFlow()
What "receiveAsFlow()" does really nicely, is that something emitted to the channel will be collected by one collector only, so a new collector (eg if your activity recreates on a rotation) will not receive the message and your user will not see it again

Observe livedata with an initial timeout

I've a livedata which emits everytime there is a update in the database. When the particular screen opens, this livedata emits immediately with whatever value is there in the database. Then, a network call is made to update the database. After the database is updated, the livedata emits again. This leads to two emissions in very quick succession. Subsequent updates to the database work properly cz there is only one emission whenever the database is updated. Only the first time, there are 2 updates in very quick succession. I want to avoid that.
An idea to avoid that would be something like this. When the livedata emits, wait for Xs. If there is another emission in those Xs, discard the data from old emission and use the new one. Wait for Xs again. If there is no emission in those Xs, use the latest data.
This looks very similiar to throttling but only once. I was wondering if there's a simple way to do something like using LiveData or MediatorLiveData.
You can post delayed Runnable with timeout you want after first LiveData event.
Every LiveData update remove posted Runnable and post it again.
You can use MediatorLiveData and a boolean val for achieving this.
Create a mDbLiveData, mediator livedata mFinalLiveData and boolean mLoadedFromAPI when data from API is loaded.
On API success or failure, set mLoadedFromAPI to true;
Observe mFinalLiveData in your Activity/Fragment
LiveData<Model> mDbLiveData;
MediatorLiveData<Model> mFinalLiveData = new MediatorLiveData();
private boolean mLoadedFromAPI = false;
// Load db data in mDbLiveData
mDbLiveData = // Data from DB
// Add mDbLiveData as source in mFinaliveData
mFinalLiveData.addSource(mDbLiveData, dbData -> {
if (mLoadedFromAPI) mFinalLiveData.postValue(dbData);
});
This post helped. https://medium.com/#guilherme.devel/throttle-operator-with-livedata-and-kotlin-coroutines-ec42f8cbc0b0
I modified the solution a bit to fit my usecase:
fun <T> LiveData<T>.debounceOnce(duration: Long,
coroutineContextProvider: CoroutineContextProvider): LiveData<T> {
return MediatorLiveData<T>().also { mediatorLivedata ->
var shouldDebounce = true
var job: Job? = null
val source = this
mediatorLivedata.addSource(source) {
if (shouldDebounce) {
job?.cancel()
job = CoroutineScope(coroutineContextProvider.IO).launch {
delay(duration)
withContext(coroutineContextProvider.Main) {
mediatorLivedata.value = source.value
shouldDebounce = false
}
}
} else {
job?.cancel()
mediatorLivedata.value = source.value
}
}
}
}
open class CoroutineContextProvider #Inject constructor() {
open val Main: CoroutineContext by lazy { Dispatchers.Main }
open val IO: CoroutineContext by lazy { Dispatchers.Default }
}

How to properly separate Realm from the rest of the app?

In my app I am trying to use MVVM with repositories databases and all that. I like to keep all my external dependencies and such separate and compartmentalized into their own files/modules so that they can easily be replaced or swapped out.
With Realm I could make this work really well by using unmanaged objects. I can have a RealmHelper class for example which just opens a realm instance, queries or performs some transaction and then closes the realm and returns an object.
So how can I accomplish something similar with managed objects? The problem is in this case that you have to know when to close the realm. The obvious solution here I think is to let the database know when you are done with it, but this seems like a tedious and unoptimized solution. Is there another better way?
So I have attempted to come up with a solution to this myself. I haven't tested it very well yet but my idea is basically to modify the LiveRealmResults file from the official example to let the caller (RealmHelper for example) know when it changes states between inactive and active. When it is active the caller will open the realm and pass in the results. When it changes to inactive the caller will close the realm. This is what my LiveRealmResults looks like:
#MainThread
class LiveRealmResults<T : RealmModel>(
private val getResults: () -> RealmResults<T>,
private val closeRealm: () -> Unit
) : LiveData<List<T>>() {
private var results: RealmResults<T>? = null
private val listener = OrderedRealmCollectionChangeListener<RealmResults<T>> {
results, _ ->
this#LiveRealmResults.value = results
}
override fun onActive() {
super.onActive()
results = getResults()
if (results?.isValid == true) {
results?.addChangeListener(listener)
}
if (results?.isLoaded == true) {
value = results
}
}
override fun onInactive() {
super.onInactive()
if (results?.isValid == true) {
results?.removeChangeListener(listener)
}
removeObserver()
}
}
It will be used like so:
class RealmHelper() {
fun getObjects(): LiveData<List<Objects>> {
var realm: Realm? = null
return LiveRealmResults<Objects>(getResults = {
realm = Realm.getDefaultInstance()
realm!!.where<Objects>().findAll()
}, removeObserver = {
realm?.close()
})
}
}
This method at least allows me to keep all realm logic in the RealmHelper, only exposing LiveData and not RealmResults. Whenever the LiveData is inactive the Realm is closed. In my example I'm returning RealmObject but I'm fine converting from RealmObject to normal object so I'm am not concerned with that part for this example.

android -MutableLiveData doesn't observe on new data

I'm using mvvm and android architecture component , i'm new in this architecture .
in my application , I get some data from web service and show them in recycleView , it works fine .
then I've a button for adding new data , when the user input the data , it goes into web service , then I have to get the data and update my adapter again.
this is my code in activity:
private fun getUserCats() {
vm.getCats().observe(this, Observer {
if(it!=null) {
rc_cats.visibility= View.VISIBLE
pb.visibility=View.GONE
catAdapter.reloadData(it)
}
})
}
this is view model :
class CategoryViewModel(private val model:CategoryModel): ViewModel() {
private lateinit var catsLiveData:MutableLiveData<MutableList<Cat>>
fun getCats():MutableLiveData<MutableList<Cat>>{
if(!::catsLiveData.isInitialized){
catsLiveData=model.getCats()
}
return catsLiveData;
}
fun addCat(catName:String){
model.addCat(catName)
}
}
and this is my model class:
class CategoryModel(
private val netManager: NetManager,
private val sharedPrefManager: SharedPrefManager) {
private lateinit var categoryDao: CategoryDao
private lateinit var dbConnection: DbConnection
private lateinit var lastUpdate: LastUpdate
fun getCats(): MutableLiveData<MutableList<Cat>> {
dbConnection = DbConnection.getInstance(MyApp.INSTANCE)!!
categoryDao = dbConnection.CategoryDao()
lastUpdate = LastUpdate(MyApp.INSTANCE)
if (netManager.isConnected!!) {
return getCatsOnline();
} else {
return getCatsOffline();
}
}
fun addCat(catName: String) {
val Category = ApiConnection.client.create(Category::class.java)
Category.newCategory(catName, sharedPrefManager.getUid())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
getCatsOnline()
}, { error ->
Log.v("this", "ErrorNewCat " + error.localizedMessage)
}
)
}
private fun getCatsOnline(): MutableLiveData<MutableList<Cat>> {
Log.v("this", "online ");
var list: MutableLiveData<MutableList<Cat>> = MutableLiveData()
list = getCatsOffline()
val getCats = ApiConnection.client.create(Category::class.java)
getCats.getCats(sharedPrefManager.getUid(), lastUpdate.getLastCatDate())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
list += success.cats
lastUpdate.setLastCatDate()
Observable.just(DbConnection)
.subscribeOn(Schedulers.io())
.subscribe({ db ->
categoryDao.insert(success.cats)
})
}, { error ->
Log.v("this", "ErrorGetCats " + error.localizedMessage);
}
)
return list;
}
I call getCat from activity and it goes into model and send it to my web service , after it was successful I call getCatsOnline method to get the data again from webservice .
as I debugged , it gets the data but it doesn't notify my activity , I mean the observer is not triggered in my activity .
how can I fix this ? what is wrong with my code?
You have made several different mistakes of varying importance in LiveData and RxJava usage, as well as MVVM design itself.
LiveData and RxJava
Note that LiveData and RxJava are streams. They are not one time use, so you need to observe the same LiveData object, and more importantly that same LiveData object needs to get updated.
If you look at getCatsOnline() method, every time the method gets called it's creating a whole new LiveData instance. That instance is different from the previous LiveData object, so whatever that is listening to the previous LiveData object won't get notified to the new change.
And few additional tips:
In getCatsOnline() you are subscribing to an Observable inside of another subscriber. That is common mistake from beginners who treat RxJava as a call back. It is not a call back, and you need to chain these calls.
Do not subscribe in Model layer, because it breaks the stream and you cannot tell when to unsubscribe.
It does not make sense to ever use AndroidSchedulers.mainThread(). There is no need to switch to main thread in Model layer especially since LiveData observers only run on main thread.
Do not expose MutableLiveData to other layer. Just return as LiveData.
One last thing I want to point out is that you are using RxJava and LiveData together. Since you are new to both, I recommend you to stick with just one of them. If you must need to use both, use LiveDataReactiveStreams to bridge these two correctly.
Design
How to fix all this? I am guessing that what you are trying to do is to:
(1) view needs category -> (2) get categories from the server -> (3) create/update an observable list object with the new cats, and independently keep the result in DB -> (4) list instance should notify activity automatically.
It is difficult to pull this off correctly because you have this list instance that you have to manually create and update. You also need to worry about where and how long to keep this list instance.
A better design would be:
(1) view needs category -> (2) get a LiveData from DB and observe -> (3) get new categories from the server and update DB with the server response -> (4) view is notified automatically because it's been observing DB!
This is much easier to implement because it has this one way dependency: View -> DB -> Server
Example CategoryModel:
class CategoryModel(
private val netManager: NetManager,
private val sharedPrefManager: SharedPrefManager) {
private val categoryDao: CategoryDao
private val dbConnection: DbConnection
private var lastUpdate: LastUpdate // Maybe store this value in more persistent place..
fun getInstance(netManager: NetManager, sharedPrefManager: SharedPrefManager) {
// ... singleton
}
fun getCats(): Observable<List<Cat>> {
return getCatsOffline();
}
// Notice this method returns just Completable. Any new data should be observed through `getCats()` method.
fun refreshCats(): Completable {
val getCats = ApiConnection.client.create(Category::class.java)
// getCats method may return a Single
return getCats.getCats(sharedPrefManager.getUid(), lastUpdate.getLastCatDate())
.flatMap { success -> categoryDao.insert(success.cats) } // insert to db
.doOnSuccess { lastUpdate.setLastCatDate() }
.ignoreElement()
.subscribeOn(Schedulers.io())
}
fun addCat(catName: String): Completable {
val Category = ApiConnection.client.create(Category::class.java)
// newCategory may return a Single
return Category.newCategory(catName, sharedPrefManager.getUid())
.ignoreElement()
.andThen(refreshCats())
.subscribeOn(Schedulers.io())
)
}
}
I recommend you to read through Guide to App Architecture and one of these livedata-mvvm example app from Google.

Categories

Resources