I'm implementing paging library with room db and coroutines , the part of loading data and handling all paging library methods is done , now i'm facing an issue which is to save the data into room db , i'm actually getting a pagedlist response from api and updating adapter with it ,and since im getting pagedlist ,i need to insert it into room db so that i can show data later in offline mode but i tried so and it didn't work , no data is showing , not sure if it is the right way to do it .
This is my response from api
mainViewModel.getAll(query,Utils().API_KEY,1).observe(viewLifecycleOwner, Observer {
newsAdapter.submitList(it) //pagedlist
recyclerView.adapter = newsAdapter
})
here is my dao
#Dao
interface NewsDao {
#Query("SELECT * FROM news_table")
fun restoreNews() : androidx.paging.DataSource.Factory<Int,AllNewsModel>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun storeNews(pagedList: PagedList<AllNewsModel>)
}
what i want to achieve is basically inserting the data of the pagedlist into my db so that i can use it to update my adapter in offline mode , thank you
I believe the best approach is to do what you want to do in your repository/boundarycallback class.
You can check this repo from google.
https://github.com/googlecodelabs/android-paging
It is a repo from a google codelab, that you can find it here
https://codelabs.developers.google.com/codelabs/android-paging/#1
You can see that class inside boundarycallback that does the job at the same time it gets the answer from the Api:
private fun requestAndSaveData(query: String) {
if (isRequestInProgress) return
isRequestInProgress = true
searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos ->
cache.insert(repos) {
lastRequestedPage++
isRequestInProgress = false
}
}, { error ->
_networkErrors.postValue(error)
isRequestInProgress = false
})
}
Related
I am using Room and I have written the Dao class as follows.
Dao
#Dao
interface ProjectDao {
#Query("SELECT * FROM project")
fun getAllProjects(): Flow<List<Project>>
...etc
}
and this Flow is converted to LiveData through asLiveData() in ViewModel and used as follows.
ViewModel
#HiltViewModel
class MainViewModel #Inject constructor(
private val projectRepo: ProjectRepository
) : ViewModel() {
val allProjects = projectRepo.allProjects.asLiveData()
...
}
Activity
mainViewModel.allProjects.observe(this) { projects ->
adapter.submitList(projects)
...
}
When data change occurs, RecyclerView is automatically updated by the Observer. This is a normal example I know.
However, in my project data in Flow, what is the most correct way to get the data of the position selected from the list?
I have already written code that returns a value from data that has been converted to LiveData, but I think there may be better code than this solution.
private fun getProject(position: Int): Project {
return mainViewModel.allProjects.value[position]
}
Please give me suggestion
Room has in built support of flow.
#Dao
interface ProjectDao {
#Query("SELECT * FROM project")
fun getAllProjects(): Flow<List<Project>>
//lets say you are saving the project from any place one by one.
#Insert()
fun saveProject(project :Project)
}
if you call saveProject(project) from any place, your ui will be updated automatically. you don't have to make any unnecessary call to update your ui. the moment there is any change in project list, flow will update the ui with new dataset.
to get the data of particular position, you can get it from adapter list. no need to make a room call.
I'm using latest Jetpack libraries.
Pagination3 version: 3.0.0-alpha05
Room Version : 2.3.0-alpha02
My entities have Long as PrimaryKey and Room can generate PagingSource for other than Int type.
error: For now, Room only supports PagingSource with Key of type Int.
public abstract androidx.paging.PagingSource<java.lang.Long, com.example.myEntity>` getPagingSource();
Therefore I tried to implement my custom PagingSource, like docs suggest.
The problem is Data Refresh, since Room's generated code handles data refresh and with my code I'm not being able to handle this scenario.
Any suggestions how to implement custom PagingSource for Room that also handles Data Refresh?
Since you have 'refresh' scenario and using Room db, I am guessing you are using Paging3 with network+local db pattern(with Room db as local cache).
I had a similar situation with network + local db pattern. I am not sure if I understand your question correctly, or your situation is the same as the one I had, but I'll share what I did anyway.
What I was using:
Paging3: 3.0.0-beta01
Room: 2.3.0-beta02
What I did was let Room library to create PagingSource (with the key of Int), and let RemoteMediator handle all the other cases, such as fetching the data from network when refreshing and/or appending, and inserting them into db right after fetch success.
My dao function for creating PagingSource from Room Library:
#Query("SELECT * FROM article WHERE isUnread = 1")
fun getUnreadPagingSource(): PagingSource<Int, LocalArticle>
In my case I defined Repository class to have dao class in its constructor to call the function above from repository when creating Pager class.
My custom RemoteMediator class looks something like this below:
Note: In my case, there is no PREPEND case so RemoteMediator#load function always returns true when the value of the argument loadType is LoadType.PREPEND.
class FeedMediator(
private val repository: FeedRepository
) : RemoteMediator<Int, LocalArticle>() {
...
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, LocalArticle>
): MediatorResult = runCatching {
when (loadType) {
LoadType.PREPEND -> true
LoadType.REFRESH -> {
feedRepository.refresh()
false
}
LoadType.APPEND -> {
val continuation = feedRepository.continuation()
if (continuation.isNullOrEmpty()) {
true
} else {
loadFeedAndCheckContinuation(continuation)
}
}
}
}.fold(
onSuccess = { endOfPaginationReached -> MediatorResult.Success(endOfPaginationReached) },
onFailure = {
Timber.e(it)
MediatorResult.Error(it)
}
)
private suspend fun loadFeedAndCheckContinuation(continuation: String?): Boolean {
val feed = feedRepository.load(continuation)
feedRepository.insert(feed)
return feed.continuation.isNullOrEmpty()
}
Finally you can create Pager class.
fun createFeedPager(
mediator: FeedMediator<Int, LocalArticle>,
repository: FeedRepository
) = Pager(
config = PagingConfig(
pageSize = FETCH_FEED_COUNT,
enablePlaceholders = false,
prefetchDistance = PREFETCH_DISTANCE
),
remoteMediator = mediator,
pagingSourceFactory = { repository.getUnreadPagingSource() }
)
I hope it helps in some way..
Other references:
https://developer.android.com/topic/libraries/architecture/paging/v3-network-db
https://android-developers.googleblog.com/2020/07/getting-on-same-page-with-paging-3.html
https://www.youtube.com/watch?v=1cwqGOku2a4
EDIT:
After reading the doc again, I found a statement where the doc clearly states:
RemoteMediator to use for loading the data from the network into the local database.
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.
I am creating an app with MVVM architecture and I ran into an issue of getting a list of LiveData to show in the View.
In my ViewModel I have a getAll() function that retrieves a list of strings from the database using Room. From there, I get the strings and call my Retrofit function to send each string individually to a web-server that returns an object. Here is where my issue occurs.
From the MVVM tutorials I see online, they usually have the LiveData> style but in this since I am getting each object individually, it becomes List> but I don't think this is the correct way of doing it because in my View I would need to do a ForEach loop to observe each LiveData object in the list.
I have tried other work arounds but it doesn't seem to work. Is there a better way of doing this?
DAO
#Query("SELECT * FROM table")
fun getAll(): LiveData<List<String>>
Repository
fun getAll(): LiveData<List<String>> {
return dao.getAll()
}
fun getRetrofitObject(s: String): LiveData<RetrofitObject> {
api = jsonApi.getRetrofitObjectInfo(s, API_KEY)
val retrofitObject: MutableLiveData<RetrofitObject> = MutableLiveData()
api.enqueue(object : Callback<RetrofitObject> {
override fun onFailure(call: Call<RetrofitObject>?, t: Throwable?) {
Log.d("TEST", "Code: " + t.toString())
}
override fun onResponse(call: Call<RetrofitObject>?, response: Response<RetrofitObject>?) {
if (response!!.isSuccessful) {
retrofitObject.value = response.body()
}
}
})
return retrofitObject
}
MainActivityViewModel (ViewModel)
var objectList ArrayList<LiveData<retrofitObject>> = ArrayList()
// This is getting objects using Retrofit
fun getRetrofitObject(s: String): LiveData<retrofitObject> {
return repo.getRetrofitObject(s)
}
// This is getting all the strings from the internal database
fun getAll(): ArrayList<LiveData<retroFitObject>> {
repo.getAll().value?.forEach {it ->
objectList.add(getRetrofitObject(it)) //How else would I be able to do this?
}
return objectList
}
MainActivity (View)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityViewModel.getAll().forEach {
it.observe(this, Observer {it ->
mainActivityViewModel.objectList.add(it) //Here is part of the issue since I don't want to use a forloop in the View
})
}
adapter.objectList = mainActivityViewModel.objectList
recyclerView.adapter = adapter
}
Thanks, let me know if there is anything else needed or confusion!
By looking at the above code you are trying to fetch a list of item for each table row from server and and trying to update the result to your recycler view. Your logic is little confusing..
So on your activity .. first init your adapter and recyclerview
Then call your viewmodel function to get all values inside table and make a loop to call your network thread fuction in background and store the values in a live data object.
Just observe this livedata in your activity/fragment and just pass the list to adapter and notify it.by doing this,whenever your livedata got a change your recyclerview also reflect the items
The problem with your code is, you are called a retrofit network function with enque option and its a background thread process.so, code wont wait for the network completion. And it will return the retrofitObject data.but it has not got the data yet.so this will make error.
There might be other methods exist I don't know about them.
But you can deal with situation using Transformations for more information please look at documentation page.
Transformations.switchMap(LiveData trigger, Function> func)
You don't have to put the live data observer inside for loop.
I have a Site and corresponding SiteDao:
#Dao
interface SiteDao {
#get:Query("SELECT * FROM site WHERE uid = 1 LIMIT 1")
val site: LiveData<Site>
#get:Query("SELECT * FROM site WHERE uid = 1 LIMIT 1")
val getSiteSync: Site
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(context: Site)
}
This works:
siteRepository.getSite().observe(activity, Observer<Site> {
// `it` is instance of Site, working as intended
})
This doesn't:
Thread {
val site = siteRepository.getSiteSync()
// site is null
}.start()
Nevermind that I'm using Repository instead of ViewModel, just an example.
Any idea why?
Room doesnt allow synchronous queries by default.
To achieve that you have to explicity call allowMainThreadQueries while initializing your database.
That is designed that way because database selection should observe for changes and ain't fetched immediately.