I use Paging Library to paginate my data set. What I'm trying to do is to refresh the RecyclerView after data in my database has been changed.
I have this LiveData:
val listItems: LiveData<PagedList<Snapshot>> = object : LivePagedListProvider<Long, Snapshot>() {
override fun createDataSource() = SnapshotsDataSource()
}.create(null, PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(PAGE_SIZE)
.setEnablePlaceholders(false)
.build()
)
And the DataSource:
class SnapshotsDataSource : KeyedDataSource<Long, Snapshot>(), KodeinGlobalAware {
val db: FirebaseDb = instance()
override fun getKey(item: Snapshot): Long = item.timestamp
override fun loadInitial(pageSize: Int): List<Snapshot> {
val result = db.getSnapshotsTail(pageSize)
return result
}
override fun loadAfter(key: Long, pageSize: Int): List<Snapshot> {
val result = db.getSnapshotsTail(key, pageSize)
return result.subList(1, result.size)
}
override fun loadBefore(key: Long, pageSize: Int): List<Snapshot> {
return emptyList()
}
}
The Adapter is straight forward, so i omit it here.
I've tried to do this when database is modified:
fun reload(position) {
listItems.value!!.loadAround(position)
}
but it didn't help.
try to call listItems.value!!.datasource.invalidate()
not directly DataSource#invalidate()
I am having the same issue with a custom Firestore based DataSource. The only way to load a portion of the data without invalidating all of the data and having the UI flash / reload seems to be via integrating with Google's Room ORM library. Unfortunately, this will cache my data twice, once with Firestore, and again with Room which is unnecessary.
See the documentation under Consider How Content Updates Work. The only way to have realtime updates is via implementing Room with the PagedList: If you're loading data directly from a Room database updates get pushed to your app's UI automatically.
It's not possible. You can invalidate the whole list only: datasource.invalidate().
Related
I have a Jetpack Compose Picture App where the user views Cached Photos from ROOM Database. The database has a isFavorite column which stores Booleans and is updated when the user clicks to like the photo.
This is my dao
#Query("SELECT * FROM astrophotoentity")
suspend fun getSavedAstroPhotos(): Flow<List<AstroPhotoEntity>>
#Query("UPDATE astrophotoentity SET isFavorite =:isFavorite WHERE id=:id")
suspend fun updateIsFavoriteStatus(id: String,isFavorite:Boolean)
This is my Repository code
override suspend fun updateIsFavoriteStatus(photo: AstroPhoto, isFavorite:Boolean) {
dao.updateIsFavoriteStatus(photo.date,isFavorite)
}
I am using a use case class for clean architecture.
class UpdateIsFavoriteStatusUseCase (private val repository: AstroRepository) {
suspend operator fun invoke(photo: AstroPhoto, isFavorite:Boolean){
repository.updateIsFavoriteStatus(photo, isFavorite)
}
}
I also use a simple data class to hold state for the ViewModel.
data class PhotoState(
val astroPhotos: List<AstroPhoto> = emptyList(),
val isPhotosListLoading: Boolean = false,
val errorMessage: String? = null)
This is the ViewModel which initializes the Photo State list to a list fetched from the DB. I use mutableStateOf() to hold the photo objects list. Here I update isFavorite column in the database depending on the passed event.
#HiltViewModel
class OverviewViewModel #Inject constructor(
private val useCaseContainer: UseCaseContainer
) : ViewModel() {
var state by mutableStateOf(PhotoState())
private set
init {
//set the state to the retrieved list of photo objects
... getPhotosListFromDb()
}
fun onEvent(event: OverviewEvent) { ....
is OverviewEvent.OnMarkFavorite -> { ....
useCaseContainer.updateIsFavoriteStatus(event.photo, event.isFavorite)}
is OverviewEvent.OnRemoveFromFavorites -> { ....
useCaseContainer.updateIsFavoriteStatus(event.photo, event.isFavorite)}
I pass the Photos list to the below composable.
#Composable
fun AstroPhotoComposable(
photo: AstroPhoto,
onMarkAsFavorite: () -> Unit,
onRemovePhoto: () -> Unit = {}
) {
//retrieve the isFavorite state using photo.isFavorite and cache it
var isFavorite by remember{ mutableStateOf(photo.isFavorite) ... }
The issue I am facing is that when the photo changes from isFavorite/!isFavorite the state is not reflecting live on the UI.
I have read somewhere about cold and hot kotlin flows but I'm yet to wrap my head around this. I also tried to replace mutableStateOf with MutableStateFlow but the state is not updating on Database Changes.
Basically, I am looking to observe for database changes and get an up-to-date state similar to LiveData.
Any help is appreciated.
Have you ever tried updating the whole entity?
#Update
suspend fun update(myEntity: myEntity): Int
And in my opinion it has to be without suspend
#Query("SELECT * FROM astrophotoentity")
fun getSavedAstroPhotos(): Flow<List<AstroPhotoEntity>>
I'd do it like that.
fun getSavedAstroPhotos() = dao.getSavedAstroPhotos().map{ it.toPhotoState() }
And collect it in view model
init {
viewModelScope.launch {
getSavedAstroPhotos().collect{
state = it
}
}
}
Perhabs it helps you link
Right now, my method of updating my jetpack compose UI on database update is like this:
My Room database holds Player instances (or whatever they're called). This is my PlayerDao:
#Dao
interface PlayerDao {
#Query("SELECT * FROM player")
fun getAll(): Flow<List<Player>>
#Insert
fun insert(player: Player)
#Insert
fun insertAll(vararg players: Player)
#Delete
fun delete(player: Player)
#Query("DELETE FROM player WHERE uid = :uid")
fun delete(uid: Int)
#Query("UPDATE player SET name=:newName where uid=:uid")
fun editName(uid: Int, newName: String)
}
And this is my Player Entity:
#Entity
data class Player(
#PrimaryKey(autoGenerate = true) val uid: Int = 0,
#ColumnInfo(name = "name") val name: String,
)
Lastly, this is my ViewModel:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val db = AppDatabase.getDatabase(application)
val playerNames = mutableStateListOf<MutableState<String>>()
val playerIds = mutableStateListOf<MutableState<Int>>()
init {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().getAll().collect {
playerNames.clear()
playerIds.clear()
it.forEach { player ->
playerNames.add(mutableStateOf(player.name))
playerIds.add(mutableStateOf(player.uid))
}
}
}
}
fun addPlayer(name: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().insert(Player(name = name))
}
}
fun editPlayer(uid: Int, newName: String) {
CoroutineScope(Dispatchers.IO).launch {
db.playerDao().editName(uid, newName)
}
}
}
As you can see, in my ViewHolder init block, I 'attach' a 'collector' (sorry for my lack of proper terminology) and basically whenever the database emits a new List<Player> from the Flow, I re-populate this playerNames list with new MutableStates of Strings and the playerIds list with MutableStates of Ints. I do this because then Jetpack Compose gets notified immediately when something changes. Is this really the only good way to go? What I'm trying to achieve is that whenever a change in the player table occurs, the list of players in the UI of the app gets updated immediately. And also, I would like to access the data about the players without always making new requests to the database. I would like to have a list of Players at my disposal at all times that I know is updated as soon as the database gets updated. How is this achieved in Android app production?
you can instead use live data. for eg -
val playerNames:Livedata<ListOf<Player>> = db.playerDao.getAll().asliveData
then you can set an observer like -
viewModel.playerNames.observe(this.viewLifecycleOwner){
//do stuff when value changes. the 'it' will be the changed list.
}
and if you have to have seperate lists, you could add a dao method for that and have two observers too. That might be way more efficient than having a single function and then seperating them into two different lists.
First of all, place a LiveData inside your data layer (usually ViewModel) like this
val playerNamesLiveData: LiveData<List<Player>>
get() = playerNamesMutableLiveData
private val playerNamesMutableLiveData = MutableLiveData<List<Player>>
So, now you can put your list of players to an observable place by using playerNamesLiveData.postValue(...).
The next step is to create an observer in your UI layer(fragment). The observer determines whether the information is posted to LiveData object and reacts the way you describe it.
private fun observeData() {
viewModel.playerNamesLiveData.observe(
viewLifecycleOwner,
{ // action you want your UI to perform }
)
}
And the last step is to call the observeData function before the actual data posting happens. I prefer doing this inside onViewCreated() callback.
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 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.