sopuse I have a function in my Room DAO like this:
#Query("SELECT * FROM cached_tbl ORDER BY id")
fun getAll(): Flowable<List<Item>>
this returns all the items in the database
but I don't want this, I want the data to be paginated and be emitted in little chunks. I want the data to be loaded from the database on demand, for example, 100 items per page. is there a way to do that?
Jetpack has a library for this called Paging which you might be interested in. The good thing about using Room is that it also has Paging integration, so setup will go something like:
Dao
#Query("SELECT * FROM cached_tbl ORDER BY id")
fun getAll(): PagingSource<Int, Item>
ViewModel
val pager = Pager(PagingConfig(pageSize)) { dao.getAll() }
.cachedIn(viewModelScope)
Activity
lifecycleScope.launch {
pager.flow.collectLatest {
PagingDataAdapter.submtiData(it)
}
}
You can read quite a bit more here: https://developer.android.com/topic/libraries/architecture/paging/v3-overview, including how to setup a layered source, transformations and more.
Related
How can you tell if paging is working properly? All the examples I've looked at involve using retrofit apiservice which appears to be returning pages of data, but I'm pulling down a single rss feed and parsing it into a giant List<POJO>. I suspect that my PagingSource is loading the entire list into one page, but I'm not sure how to tell.
My list has near 1000 items, so I assume it'd be good practice to implement some kind of paging/DiffUtil. I'm playing around in this with jetpack compose usingandroidx.paging:paging-compose:1.0.0-alpha12 which probably complicates things.
Can anyone give me some pointers?
class RssListSource(): PagingSource<Int, RssItem>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RssItem> {
return try {
val nextPage = params.key ?: 1
val rssList: List<RssItem> = RssFeedFetcher().fetchRss()
LoadResult.Page(
data = rssList,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = nextPage.plus(1)
)
} catch (e: Exception){
LoadResult.Error(e)
}
}
}
class MainActivityViewModel: ViewModel() {
val rss: Flow<PagingData<RssItem>> = Pager(PagingConfig(pageSize = 10)){
RssListSource() // returned to LazyPagingItems list in #Composable
}.flow.cachedIn(viewModelScope)
}
Your data still needs a way to fetch pages of data. I would expect your RssFeedFetcher to use the page information and return a page accordingly.
You are probably correct that you are currently returning all items at once.
There's two main strategies here:
Add a long enough delay() to load() such that you have enough time to scroll to the end of the list before new page loads
class RssListSource(): PagingSource<Int, RssItem>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RssItem> {
delay(5000)
...
}
Observe changes to LazyPagingItems.loadState and look for either PREPEND or APPEND switching between Loading and NotLoading.
In order for you to be able to implement pagination with the paging library you need to use a paginated API, that means that in your example, you'd need a way to fetch the RSS in a paginated fashion. The Paging library won't be able to make use of pagination if your data source does not provide a way to query for paginated data, unfortunately.
You could achieve what you want by implementing a middleware that fetches the RSS feed and splits it into pages for you to query from the Android app.
EDIT: Another approach could be to have a background task (using the Android WorkManager) to download the RSS feed and save it in a Room Database, then use the Paging library to load pages off the database. Here's a summary of how to show paginated data from a Room database: https://developer.android.com/topic/libraries/architecture/paging/v3-network-db
I'm new to android programming and want to try to learn best practices. My first app I'm building is a podcast app to display podcasts from an rss feed and play them. What I have so far is working, but I know I can make it work better.
I'm using a Room Database with a Repository pattern, which might be overkill because I probably don't need to persist the podcast list across app death if I'm just going to re-parse the feed on startup. In my repository class I'm calling my FetchRSS class to do the network call in the init{ } block which returns a List<Podcast>.
I know I'm not doing something right.
In my PodcastDao, I have to use #Insert(onConflict = OnConflictStrategy.REPLACE) because the database already exists and I get an SQL error 1555 regarding duplicate primary key ids. Logically, it'd be better to have a check to see if the entry to be added is already in the database, but I'm not sure how to go about doing that. Or, illogically, clear the database on app death, but then why bother with a database at all. Ideally, I'd like to have a swipe to update function(even if the RSS only updates at most twice a week), but I'm not sure how best to do that.
If anyone has any thoughts about improving this, or a good book for learning android, I'd be all ears.
Thank you so much to everyone who takes the time to look at this!
PodcastDao.kt
#Dao
interface PodcastDao {
#Query("SELECT * FROM podcast") // get everything from the database
fun getPodcasts(): LiveData<List<Podcast>>
#Query("SELECT * FROM podcast WHERE id=(:id)") // get the specific podcast
fun getPodcast(id: String): LiveData<Podcast?>
// #Insert(onConflict = OnConflictStrategy.REPLACE)
// fun addPodcasts(podcasts: LiveData<List<Podcast>>)
// this causes a build error with the generated PodcastDao.java file
// logcat error: Type of the parameter must be a class annotated with #Entity or a collection/array of it.
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun addPodcast(podcast: Podcast)
}
PodcastRepository.kt
class PodcastRepository private constructor(context: Context) {
private lateinit var podcasts: List<Podcast>
init {
CoroutineScope(Dispatchers.Main).launch {
podcasts = FetchRSS().fetchRss() // executes on Dispatchers.IO and returns parsed rss List<Podcast>
// this seems silly to add them one at a time, especially since the list is rather large
for (pod in podcasts) {
addPodcast(pod)
}
//it seems a better choice to dump the full list into the database at once
//however I can't figure out how to put the List<Podcast> into a LiveData<List<Podcast>> object
//or maybe I'm misunderstanding something about LiveData<>
//addPodcasts(podcasts)
}
}
suspend fun addPodcast(podcast: Podcast){
withContext(Dispatchers.IO){
podcastDao.addPodcast(podcast)
}
// this needs to receive the LiveData<List<Podcast>>, or a List<Podcast> and cram it into LiveData<>?
// suspend fun addPodcasts(podcasts: LiveData<List<Podcast>>) {
// withContext(Dispatchers.IO){
// podcastDao.addPodcasts(podcasts)
// }
// }
}
fun addPodcasts(podcasts: LiveData<List<Podcast>>)
should be
fun addPodcasts(podcasts: <List<Podcast>>)
So, now you can call podcastDao.addPodcasts(podcasts) (where podcasts is of type List<Podcast>>) from inside your repository instead of inserting them one by one through a for loop.
You cannot insert a LiveData into Room, only objects marked with #Entity. You can, however, have a query return a LiveData with a List of those entities. You can also return just a List as well.
For now I use this code in my DAO:
#Dao
interface NotesDao {
#Query("SELECT * FROM Notes")
fun getAllNotes(): List<Notes?>?
#Query("SELECT * FROM Notes WHERE not hidden AND not grouped")
fun getNotes(): List<Notes?>?
#Query("SELECT * FROM Notes WHERE id = :id")
fun getById(id: Long): Notes?
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(notes: Notes?)
#Update
fun update(notes: Notes?)
#Delete
fun delete(notes: Notes?)
}
But in many tutorials I either see Flowable<List<Notes>> instead of List<Notes> or LiveData<List<Notes>>.
Which approach is preferable?
There are two factors you should consider:
All interactions with database (both queries and updates) should be outside main thread.
In some use-cases you want single "one-shot" query, in other - you want your query to be "live" - i.e. to return updated result whenever data was changed (Observer pattern).
Using Kotlin you have two main options (what's better is a matter of taste. In some tutorials you can see also AsyncTask or Thread executors for switching off the main thread):
Use RxJava (use Flowable for "live" queries, Single - for "one-shot" queries and one of the Single/Completable/Maybe for insert/update/delete). Using RxJava you can use built-in mechanism for switching off the main thread.
Use Coroutines (and keyword suspend) for insert/update/delete, and LiveData or Flow (from coroutines library) for "live" queries, or suspend - for "one-shot" queries. suspend lets to switch from the main thread. If you use LiveData/Flow you don't need suspend, since Room does it itself.
See also official documentation for additional hints.
I made a simple example app with using Room and Flows:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val build = Room.databaseBuilder(this, FinanceDatabase::class.java, "database.db")
.fallbackToDestructiveMigration()
.build()
GlobalScope.launch {
build.currencyDao().addCurrency(CurrencyLocalEntity(1))
val toList = build.currencyDao().getAllCurrencies().toList()
Log.d("test", "list - $toList")
}
}
}
#Entity(tableName = "currency")
data class CurrencyLocalEntity(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "currencyId")
var id: Int
) {
constructor() : this(-1)
}
#Dao
interface CurrencyDao {
#Query("SELECT * FROM currency")
fun getAllCurrencies(): Flow<CurrencyLocalEntity>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addCurrency(currency: CurrencyLocalEntity)
}
#Database(entities = [CurrencyLocalEntity::class], version = 1)
abstract class FinanceDatabase : RoomDatabase() {
abstract fun currencyDao(): CurrencyDao
}
I want to use toList() function as in code above but something gets wrong and even Log doesn't print. At the same time using collect() works fine and gives me all records.
Can anybody explain to me what is wrong? Thanks.
There are a couple things wrong here but I'll address the main issue.
Flows returned by room emit the result of the query everytime the database is modified. (This might be scoped to table changes instead of the whole database).
Since the database can change at any point in the future, the Flow will (more or less) never complete because a change can always happen.
Your calling toList() on the returned Flow will suspend forever, since the Flow never completes. This conceptually makes sense since Room cannot give you the list of every change that will happen, without waiting for it to happen.
With this, I'm sure you know why collect gives you the records and toList() doesn't.
What you probably want here is this.
#Query("SELECT * FROM currency")
fun getAllCurrencies(): Flow<List<CurrencyLocalEntity>>
With this you can get the first result of the query with Flow<...>.first().
Flow in Room is for observing Changes in table.
Whenever any changes are made to the table, independent of which row is changed, the query will be re-triggered and the Flow will emit again.
However, this behavior of the database also means that if we update an unrelated row, our Flow will emit again, with the same result. Because SQLite database triggers only allow notifications at table level and not at row level, Room can’t know what exactly has changed in the table data
Make sure that the same doa object you are using for retrieving the list, is used for updating the database.
other than that converting flow to livedata is done using asLivedata extension function
For me below solution works for updating the view with database table changes.
Solution: Same Dao Object should be used when we insert details into the room database and get information from DB.
If you are using a dagger hilt then
#Singleton annotation will work.
I hope this will solve your problem.
**getAllCurrencies()** function should be suspend.
Please check the syntax to collect List from Flow:
suspend fun <T> Flow<T>.toList(
destination: MutableList<T> = ArrayList()
): List<T> (source)
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/to-list.html
So my general question is how to call a function from view model for #Query where you have to pass something and then return something.
My simple example:
DAO
#Query ("SELECT * FROM table_name WHERE id = :id LIMIT 1")
fun getItemById (id: Long) : MyItem
Repo
fun getItemById (id: Long) : MyItem {
return itemDao.getItemById(id)
}
I know that it cannot and should not be done on ui thread. For inserting and deleting an item i use viewModelScope job but i cannot (maybe just don`t know how to) use it to return anything.
If i return it everywhere as LiveData, then it works just like that:
ViewModel
fun itemById(id: Long): LiveData<MyItem> {
return itemRepo.getItemById(id)
}
And then i observe it in a Fragment/Activity:
viewModel.itemById(id).observe(this, Observer {
// using it
})
The thing is, that i dont really need it to be an observable livedata. I only need to get it once, check condition and thats it.
So maybe someone could recommend how to do it, without it being a livedata. Or should i leave it a live data?
If you want to get the update only once, then I recommend SingleLiveEvent instead of LiveData.
Here is the class provided by google: Github link
A blog on how to use it: Link
The only drawback of SingleLiveEvent is that it can't have multiple observers.
If you don't like LiveData, you could try RxJava's Single [Observable]