I'm working on Messaging app and I am implementing database + network to save chat messages from api and show them from database. I'm using BoundaryCallback to fetch message when database has no more data. My api works like this:
getlist( #Query("msgid") long msgid,
#Query("loadolder") boolean olderOrNewer,
#Query("showcurrentMessage") boolean showcurrentMessage,
#Query("MsgCountToLoad") int MsgCountToLoad);
msgid : last message id of that chat . if db is empty I request
with the chat.getlastmessageid if the db has data but there was no
more data I will send last message id in db to load more and if first
time opening chat, the last message id in db was not equal to
chat.lastmessageid it's mean there is new message to load.
loadolder : this flag false to tell api load new messages from this
message id I sent to you and on and if flag set to true means load
older messages from this message id I sent to you
showcurrentMessage: if true it will give me the current message (msgid) too
MsgCountToLoad : how many messages to take from api
question is how to handle this stuff in Pagginglibrary? How to tell it to load older or newer message which is based on scrolling position. First time to load data is easy, it will returns null object so I will use the chat.lastmessageid next time opening chat where I could check if chat.lastmessageid is equal to db.lastmessageid and tell it to load more new messages.
PagedList.BoundaryCallback has two separate APIs for prepending and appending.
You should look to implement these methods:
onItemAtEndLoaded
onItemAtFrontLoaded
Assuming your initial load loads the most recent messages, and scrolling up loads older messages, you can just pass true for loadolder in onItemAtFrontLoaded and false in onItemAtEndLoaded.
I'm working my last project on Messaging app. One the most important and common things that we do in our projects is to load data gradually from the network or the database maybe because there is a huge number of entities that can’t be loaded at once.
If you are not familiar with paging library or live data concepts please take your time to study them first because I’m not going to talk about them here. There are lots of resources you can use to learn them.
My solution consists of two main parts!
Observing the database using Paging Library.
Observing the RecyclerView to understand when to request the server for data pages.
For demonstration we are going to use an entity class that represents a Person:
#Entity(tableName = "persons")
data class Person(
#ColumnInfo(name = "id") #PrimaryKey val id: Long,
#ColumnInfo(name = "name") val name: String,
#ColumnInfo(name = "update_time") val updateTime: Long
)
1. Observe the database
Lets start with the first and easier one:
To observe the database we are going to define a method in our dao that returns a DataSource.Factory<Int, Person>
#Dao
interface PersonDao {
#Query("SELECT * FROM persons ORDER BY update_time DESC")
fun selectPaged(): DataSource.Factory<Int, Person>
}
And now in our ViewModel we are going to build a PagedList from our factory
class PersonsViewModel(private val dao: PersonDao) : ViewModel() {
val pagedListLiveData : LiveData<PagedList<Person>> by lazy {
val dataSourceFactory = personDao.selectPaged()
val config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.build()
LivePagedListBuilder(dataSourceFactory, config).build()
}
}
And from our view we can observe the paged list
class PersonsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_persons)
viewModel.pagedListLiveData.observe(this, Observer{
pagedListAdapter.submitList(it)
})
}
}
Alright, now that is basically what we should do for the first part. Please notice that we are using a PagedListAdapter. Also we can do some more customization on our PagedList.Config object but for simplicity we omit it. Again please notice that we didn’t use a BoundaryCallback on our LivePagedListBuilder.
2. Observe the RecyclerView
Basically what we should do here is to observe the list, and based on where in the list we are right now, ask the server the provide us with the corresponding page of data. For observing the RecyclerView position we are going to use a simple library called Paginate.
class PersonsActivity : AppCompatActivity(), Paginate.Callbacks {
private var page = 0
private var isLoading = false
private var hasLoadedAllItems = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_persons)
viewModel.pagedListLiveData.observe(this, Observer{
pagedListAdapter.submitList(it)
})
Paginate.with(recyclerView, this).build()
}
override fun onLoadMore() {
// send the request to get corresponding page
}
override fun isLoading(): Boolean = isLoading
override fun hasLoadedAllItems(): Boolean = hasLoadedAllItems
}
As you can see we bound the Paginate with the recycler view and now we have three callbacks. isLoading() should return the state of network. hasLoadedAllItems() shows whether or not we have reached the last page and there is no more data to load from the server. Most of what we do is implementing the last method onLoadMore().
In this stage we should do three things:
Based on the recyclerView position, we ask the server to present us with the right data page.
Using the fresh data from the server we update the database, resulting in updating the PagedList and showing the fresh data. Don’t forget we are observing the database!
If the request fails we show the error.
With these simple steps we solve two problems.
First of all despite the BoundaryCallbak, that doesn’t have a callback to fetch the already fetched data, we are requesting each page on demand so we can notice the updated entities and also update our own local databse.
Second we can easily show the state of the network and also show the possible network failures.
Sounds fine right? Well we haven’t solved one particular problem yet. And that is what if one entity gets deleted from the remote server. How are we going to notice that! Well that is where ordering of data comes in. With a really old trick of sorting the data we can notice the gaps between our persons. For example we can sort our persons based on their update_time now if the returned JSON page from the server looks like this:
{
"persons": [{
"id": 1,
"name": "Reza",
"update_time": 1535533985000
}, {
"id": 2,
"name": "Nick",
"update_time": 1535533985111
}, {
"id": 3,
"name": "Bob",
"update_time": 1535533985222
}, {
"id": 4,
"name": "Jafar",
"update_time": 1535533985333
}, {
"id": 5,
"name": "Feryal",
"update_time": 1535533985444
}],
"page": 0,
"limit": 5,
"hasLoadedAllItems": false
}
Now we can be sure that whether if there is a person in our local database that its update_time is between the first and the last person of this list, but it is not among these persons, is in fact deleted from the remote server and thus we should delete it too.
I hope I was too vague but take a look at the code below
override fun onLoadMore() {
if (!isLoading) {
isLoading = true
viewModel.loadPersons(page++).observe(this, Observer { response ->
isLoading = false
if (response.isSuccessful()) {
hasLoadedAllItems = response.data.hasLoadedAllItems
} else {
showError(response.errorBody())
}
})
}
}
But the magic happens in the ViewModel
class PersonsViewModel(
private val dao: PersonDao,
private val networkHelper: NetworkHelper
) : ViewModel() {
fun loadPersons(page: Int): LiveData<Response<Pagination<Person>>> {
val response =
MutableLiveData<Response<Pagination<Person>>>()
networkHelper.loadPersons(page) {
dao.updatePersons(
it.data.persons,
page == 0,
it.hasLoadedAllItems)
response.postValue(it)
}
return response
}
}
As you can see we emit the network result and also update our database
#Dao
interface PersonDao {
#Transaction
fun updatePersons(
persons: List<Person>,
isFirstPage: Boolean,
hasLoadedAllItems: Boolean) {
val minUpdateTime = if (hasLoadedAllItems) {
0
} else {
persons.last().updateTime
}
val maxUpdateTime = if (isFirstPage) {
Long.MAX_VALUE
} else {
persons.first().updateTime
}
deleteRange(minUpdateTime, maxUpdateTime)
insert(persons)
}
#Query("DELETE FROM persons WHERE
update_time BETWEEN
:minUpdateTime AND :maxUpdateTime")
fun deleteRange(minUpdateTime: Long, maxUpdateTime: Long)
#Insert(onConflict = REPLACE)
fun insert(persons: List<Person>)
}
Here in our dao first we delete all the persons that their updateTime is between the first and last person in the list returned from the server and then insert the list into the database. With that we made sure any person that is deleted on the server is also deleted in our local database as well. Also notice that we are rapping these two method calls inside a database #Transaction for better optimization. The changes of the database will be emitted through our PagedList thus updating the ui and with that we are done.
Related
Hey I want to call two different api for my Paging Library 3. I want to ask what is best suit for me to use Paging Source or Remote Mediator?. What is the use case of both? Can someone please explain me.
For 1st api call only for single time
#GET("/movie?min=20")
Above api call returns this response
data class movie(
var id: Int?,
var name: String?,
var items : List<Genre>?
}
Now for 2nd api call its loop to call again and again
#GET("/movie?count=20&&before={time}")
Above api call retrun this
data class movie(
var items : List<Genre>?
}
Genre
data class Genre(
var type: String?,
var date: String?,
var cast: String?
}
Genre have data in both api call. I tried to google this and found this Example. But inside this both api return same data. But in my case both returns little bit different. Also id, name is only used in UI component else list will go to adapter. But I didn't understand how to achieved this. I am new in Flow, it too difficult to understand, to be honest I am trying to learning CodeLab. Another important thing when 1st time api call, in which the last item contains date will send to 2nd api call in time parameter and then 2nd api last item date call again 2nd api, this will go in loop. So how can I track this again in loop condition. Third I want to update data at top of list, can we store data in memory than we can update value on that list? Thanks for advance. Sorry for my wrong english.
UPDATE
After #dlam suggestion, I tried to practice some code
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel by viewModels<ActivityViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launchWhenCreated {
viewModel.getMovie().collectLatest {
// setupAdapter()
}
}
}
}
ActivityViewModel
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
fun getMovie(): Flow<PagingData<Genre>> {
return Pager(
config = PagingConfig(
pageSize = 20
),
pagingSourceFactory = {
MultiRequestPagingSource(DataSource())
}
).flow
}
}
MultiRequestPagingSource
class MultiRequestPagingSource(private val dataSource: DataSource) : PagingSource<String, Genre>() {
override fun getRefreshKey(state: PagingState<String, Genre>): String? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.nextKey
}
}
override suspend fun load(params: LoadParams<String>): LoadResult<String, Genre> {
val key = params.key ?: ""
return try {
val data = when (params) {
is LoadParams.Refresh -> {
dataSource.fetchInitialMovie()
}
is LoadParams.Append -> {
dataSource.fetchMovieBefore(key)
}
is LoadParams.Prepend -> null
}
LoadResult.Page(
data = data.result,
prevKey = null,
nextKey = data?.nextKey,
)
} catch (exception: IOException) {
LoadResult.Error(exception)
}
}
}
I am getting error on data = data.result
Type mismatch.
Required:
List<TypeVariable(Value)>
Found:
ArrayDeque<Genre>?
DataSource
package com.example.multirequestpaging
class DataSource {
data class MovieResult(
val result: ArrayDeque<Genre>?,
val nextKey: String?
)
fun fetchInitialMovie(): MovieResult {
val response = ApiInterface.create().getMovieResponse(20)
return MovieResult(
addInArrayDeque(response),
response.items?.last()?.date
)
}
fun fetchMovieBefore(key: String): MovieResult {
val response = ApiInterface.create().getMovieResponseBefore(20, key)
return MovieResult(
addInArrayDeque(response),
response.items?.last()?.date
)
}
private fun addInArrayDeque(response: MovieResponse): ArrayDeque<Genre> {
val result: ArrayDeque<Genre> = ArrayDeque()
response.items?.forEach {
result.add(it)
}
return result
}
}
For Full code Project Link
1. I want to add an item to the top of the list. How can I use invalidate function? Sorry I didn't understand where I can use.
2. I want to use id,name in other place so how can i get those variable value in my activity class.
3. Is my code structure is good?. Do I need to improved, please give an example. It will also help beginner, who is learning Paging Library.
Thanks
PagingSource is the main driver for Paging, it's responsible for loading items that get displayed and represents the single source of truth of data.
RemoteMediator is for layered sources, it is essentially a callback which triggers when PagingSource runs out of data, so you can fetch from a secondary source. This is primarily useful in cases where you fetching from both DB + Network, where you want locally cached data to power Paging, and then use RemoteMediator as a callback to fetch more items into the cache from network.
In this scenario you have two APIs, but they both fetch from the same Network source, so you only need PagingSource here. If I'm understanding correctly, you essentially want to call the first API on initial load and the second API on subsequent prepend / append page loads, which you can check / switch on by the type of LoadParams you get. See the subtypes here: https://developer.android.com/reference/kotlin/androidx/paging/PagingSource.LoadParams
I am struggling with the Paging 3 Library of Jetpack.
I setup
Retrofit for the network API calls
Room to store the retrieved data
A repository that exposes the Pager.flow (see code below)
A RemoteMediator to cache the network results in the room database
The PagingSource is created by Room.
I understand that the RemoteMediators responsibility is to fetch items from the network and persist them into the Room database. By doing so, we can use the Room database as single point of truth. Room can easily create the PagingSource for me as long as I am using Integers as nextPageKeys.
So far so good. Here is my ViewModel to retrieve a list of Sources:
private lateinit var _sources: Flow<PagingData<Source>>
val sources: Flow<PagingData<Source>>
get() = _sources
private fun fetchSources() = viewModelScope.launch {
_sources = sourcesRepository.getSources(
selectedRepositoryUuid,
selectedRef,
selectedPath
)
}
val sources is collected in the Fragment.
fetchSources() is called whenever one of the three parameters change (selectedRepositoryUuid, selectedRef or selectedPath)
Here is the Repository for the Paging call
fun getSources(repositoryUuid: String, refHash: String, path: String): Flow<PagingData<Source>> {
return Pager(
config = PagingConfig(50),
remoteMediator = SourcesRemoteMediator(repositoryUuid, refHash, path),
pagingSourceFactory = { sourcesDao.get(repositoryUuid, refHash, path) }
).flow
}
Now what I experience is that Repository.getSources is first called with correct parameters, the RemoteMediator and the PagingSource are created and all is good. But as soon as one of the 3 parameters change (let's say path), neither the RemoteMediator is recreated nor the PagingSource. All requests still try to fetch the original entries.
My question: How can I use the Paging 3 library here in cases where the paging content is dependent on dynamic variables?
If it helps to grasp my use-case: The RecyclerView is displaying a paged list of files and folders. As soon as the user clicks on a folder, the content of the RecyclerView should change to display the files of the clicked folder.
Update:
Thanks to the answer of dlam, the code now looks like this. The code is a simplification of the real code. I basically encapsulate all needed information in the SourceDescription class.:
ViewModel:
private val sourceDescription = MutableStateFlow(SourceDescription())
fun getSources() = sourceDescription.flatMapConcat { sourceDescription ->
// This is called only once. I expected this to be called whenever `sourceDescription` emits a new value...?
val project = sourceDescription.project
val path = sourceDescription.path
Pager(
config = PagingConfig(30),
remoteMediator = SourcesRemoteMediator(project, path),
pagingSourceFactory = { sourcesDao.get(project, path) }
).flow.cachedIn(viewModelScope)
}
fun setProject(project: String) {
viewModelScope.launch {
val defaultPath = Database.getDefaultPath(project)
val newSourceDescription = SourceDescription(project, defaultPath)
sourceDescription.emit(newSourceDescription)
}
}
In the UI, the User first selects a project, which is coming from the ProjectViewModel via LiveData. As soon as we have the project information, we set it in the SourcesViewModel using the setProject method from above.
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// load the list of sources
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
sourcesViewModel.getSources().collectLatest { list ->
sourcesAdapter.submitData(list) // this is called only once in the beginning
}
}
projectsViewModel.projects.observe(viewLifecycleOwner, Observer { project ->
sourcesViewModel.setProject(project)
})
}
The overall output of Paging is a Flow<PagingData>, so typically mixing your signal (file path) into the flow via some flow-operation will work best. If you're able to model the path the user clicks on as a Flow<String>, something like this might work:
ViewModel.kt
class MyViewModel extends .. {
val pathFlow = MutableStateFlow<String>("/")
val pagingDataFlow = pathFlow.flatMapLatest { path ->
Pager(
remoteMediator = MyRemoteMediator(path)
...
).flow.cachedIn(..)
}
}
RemoteMediator.kt
class MyRemoteMediator extends RemoteMediator<..> {
override suspend fun load(..): .. {
// If path changed or simply on whenever loadType == REFRESH, clear db.
}
}
The other strategy if you have everything loaded is to pass the path directly into PagingSource, but it sounds like your data is coming from network so RemoteMediator approach is probably best here.
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.
I'm trying to move Android project to MVVM. Two different repositories get same data response, when calling different server endpoints. What is the right way to keep both up-to-date?
There are different server endpoints returning "UserData": "/get_user_data" and "/get_user_statistics". I need to update data in "UserRepository", when making request in "UserStatisticsRepository" (which returns UserData as well).
Should I inject both Repositories in a ViewModel and then set the "UserData" via ViewModel to "UserRepository"? It just feels to me not really right way to set data to a Repository from a ViewModel...
Let's say, I have:
data class UserData(
#SerializedName("id") val id: Int,
#SerializedName("name") val name: String
)
and
data class UserStatisticData(
#SerializedName("id") val id: Int,
#SerializedName("active_users_count") val activeUsersCount: Int,
#SerializedName("users") val users: List<UserData>
)
and
class UserStatisticRepository(...) {
...
suspend fun makeUserStatisticDataRequest(){
withContext(Dispatchers.IO) {
val networkResponse = myAppAPI?.getUserStatisticData(params)
try {
if (networkResponse!!.isSuccessful) {
_userStatisticData.value = networkResponse.body()?.userStatisticData
// TODO: Should I set UserData (networkResponse.body()?.userData) to UserRepository here???
// How can I access UserRepository?
} else {
throw UserStatisticDataResponseError(Exception("Response body error..."))
}
} catch (e: Exception) {
throw UserStatisticDataResponseError(Exception("Network error..."))
}
}
}
...
}
I am not sure how your projwct is set up but If I was in this kind of situation I would :
Either have a single state that is mutated by both of the repository, make it reactive. Both of the repostitory can take that state as argument when being constructed and they can also mutate the state.
A reactive database that is mutated by both the calls. i.e. whenever any new UserData entity is fetched, its written to databse. When we need UserData we listen it directly from Reactive Database like "Room" instead of those API endpoints directly.
When to do what?
I would choose the first option if I did not have to persist the fetched data & I am cool with losing all the data once app is closed where as If I wanted the data from those endpoints to survive App restart the I would go for the second options.
For the first option the code may look like this :
data class UserDataSource(val userDataList : List<UserData>)
class UserDataRepo(val subject : PublishSubject<UserDataSource>){
UserdataService.getUserData(userID).subscribe{
// Do calculations to find the old list of user and edit or update the new user
subject.onNext(UserDataSource(newUserList))
}
}
class UserStatRepo(subject : PublishSubject<UserDataSource>){
UserdataService.getUserStat().subscribe{
// Do calculations to find the old list of user and edit or update the new user
subject.onNext(UserDataSource(newUserList))
}}
In Ui subscribe to the userDataSource subject..
class UserDataViewModel{
val userDataSource = PublishSubject.create<userDataSource>()
val userDataRepository = UserDataRepo (userDataSource)
val userDataRepository = UserStatRepo (userDataSource)
}
my app architecture, quite common:
Please explain me if I have list of Entities, for example
#Entity(tableName = TABLE_NAME)
class Item constructor(
#PrimaryKey(autoGenerate = false)
var id: Long = 0L,
#TypeConverters(ListToStringConverter::class)
var eventDescription: List<String> = emptyList(),
#TypeConverters(DateConverter::class)
var date: Date = Date(),
var rate: Int ?= null)
Picture explanation:
Currently I do (according to picture above):
mLiveData getLiveData from Repository
callbackrefreshFromDataBase()
mLiveData.addSource from LiveData of DataBase - what causes that Android View is updated quickly
callback refreshFromNetwork()
Rest updates DatabaseTable
Database insert causes that LiveData add pushes changes to the View
Formulation of the problem
What is the best practice for 5 step - a moment when new data comes and I have to replace old data with the freshest?
Currently I'm using RxJava, Room, Kotlin and I'm using in step 3 nested Rx.Single which is very ugly spagetti code.
like
disposable = usecase.getDataFromNetwork()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onSuccess = {
val itemsList = it
dataBase.deleteAllItems()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy (onComplete = { dataBase.insertNewItems(itemsList)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onComplete = {
// Process complete
}, onError = {
...
})
}
}, onError = {
...
})
}, onError = {
...
})
Ugly spagetti code.
QUESTIONS
Should I wipe all Table before inserting new Items? Livedata listen to the changes of List, so whenever anything changes (some item updated, some inserted, some removed) view will be updated
Under Circumstances where all my Streams are Single (reason: they dispose by itself when onComplete), how to chain them sequentially in oder to restrict one stream rule?
This depends if your fetched data will be similar or not. If you fresh data is normally completly different I would delete all elements and then insert new elements with only one method like following:
#Insert
public void insertItems(Item... items);
If your new data is similar and you don't care about their order I would compare all new items to the old ones to know for each item if you have to update, insert or delete item. At the end you call deleteItems(itemsToDelete), updateItems(itemsToUpdate) and insertItems(itemsToInsert). This three methods are similar as the one written above but with #Delete, #Update and #Insert.
If you care about the order I recommend you take a look at Android Paging Library and create a custom DataSource where you can tell to your live data when to update its values. Some links to start: this, this, this and this