Database repository comparison and/or update in Android Kotlin? - android

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.

Related

Is room capable of getting data from dataclasses?

this might be a basic question but I cannot find any way to do it and I am not even sure whether that it is possible:
let's say I have a data class named "LoginCredentials", which takes 2 values: username and password (both strings)
could I make a query that looks like that?:
#Query("SELECT * FROM X WHERE username = :loginCredentials.username AND password = :loginCredentials.password")
or is there any other way to do it?
I don't think it's possible - I'm not a Room expert, but the documentation only mentions referencing parameters and expanding collections in a #Query string. There's nothing about using more complex, arbitary data structures.
I think the way you're meant to do it is with a public method in your DAO that takes your data structure, and have that internally call another function that takes the individual parameters:
#Dao
interface MyDao {
#Query("SELECT * FROM X WHERE username = :username AND password = :password")
fun getFromUserAndPass(username: String, password: String): Whatever
fun getFromLoginCreds(creds: LoginCredentials) =
getFromUserAndPass(creds.username, creds.password)
}
There's no way to make that "internal" version private that I know of (adding it to an interface disallows that, not sure if there's an alternative) but you can always abstract access to your DAO through a repository layer or something, with its own API

Android Room library with Kotlin Flow toList() doesn't work

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

Kotlin Coroutines Flow with Room and state handling

I'm trying out the new coroutine's flow, my goal is to make a simple repository that can fetch data from a web api and save it to db, also return a flow from the db.
I'm using room and firebase as the web api, now everything seems pretty straight forward until i try to pass errors coming from the api to the ui.
Since i get a flow from the database which only contains the data and no state, what is the correct approach to give it a state (like loading, content, error) by combining it with the web api result?
Some of the code i wrote:
The DAO:
#Query("SELECT * FROM users")
fun getUsers(): Flow<List<UserPojo>>
The Repository:
val users: Flow<List<UserPojo>> = userDao.getUsers()
The Api call:
override fun downloadUsers(filters: UserListFilters, onResult: (result: FailableWrapper<MutableList<UserApiPojo>>) -> Unit) {
val data = Gson().toJson(filters)
functions.getHttpsCallable("users").call(data).addOnSuccessListener {
try {
val type = object : TypeToken<List<UserApiPojo>>() {}.type
val users = Gson().fromJson<List<UserApiPojo>>(it.data.toString(), type)
onResult.invoke(FailableWrapper(users.toMutableList(), null))
} catch (e: java.lang.Exception) {
onResult.invoke(FailableWrapper(null, "Error parsing data"))
}
}.addOnFailureListener {
onResult(FailableWrapper(null, it.localizedMessage))
}
}
I hope the question is clear enough
Thanks for the help
Edit: Since the question wasn't clear i'll try to clarify. My issue is that with the default flow emitted by room you only have the data, so if i were to subscribe to the flow i would only receive the data (eg. In this case i would only receive a list of users). What i need to achieve is some way to notify the state of the app, like loading or error. At the moment the only way i can think of is a "response" object that contains the state, but i can't seem to find a way to implement it.
Something like:
fun getUsers(): Flow<Lce<List<UserPojo>>>{
emit(Loading())
downloadFromApi()
if(downloadSuccessful)
return flowFromDatabase
else
emit(Error(throwable))
}
But the obvious issue i'm running into is that the flow from the database is of type Flow<List<UserPojo>>, i don't know how to "enrich it" with the state editing the flow, without losing the subscription from the database and without running a new network call every time the db is updated (by doing it in a map transformation).
Hope it's clearer
I believe this is more of an architecture question, but let me try to answer some of your questions first.
My issue is that with the default flow emitted by room you only have
the data, so if i were to subscribe to the flow i would only receive
the data
If there is an error with the Flow returned by Room, you can handle it via catch()
What i need to achieve is some way to notify the state of the app,
like loading or error.
I agree with you that having a State object is a good approach. In my mind, it is the ViewModel's responsibility to present the State object to the View. This State object should have a way to expose errors.
At the moment the only way i can think of is a "response" object that
contains the state, but i can't seem to find a way to implement it.
I have found that it is easier to have the State object that the ViewModel controls be responsible for errors instead of an object that bubbles up from the Service layer.
Now with these questions out of the way, let me try to propose one particular "solution" to your issue.
As you mention, it is common practice to have a Repository that handles retrieving data from multiple data sources. In this case, the Repository would take the DAO and an object that represents getting data from the network, let's call it Api. I am assuming that you are using FirebaseFirestore, so the class and method signature would look something like this:
class Api(private val firestore: FirebaseFirestore) {
fun getUsers() : Flow<List<UserApiPojo>
}
Now the question becomes how to turn a callback based API into a Flow. Luckily, we can use callbackFlow() for this. Then Api becomes:
class Api(private val firestore: FirebaseFirestore) {
fun getUsers() : Flow<List<UserApiPojo> = callbackFlow {
val data = Gson().toJson(filters)
functions.getHttpsCallable("users").call(data).addOnSuccessListener {
try {
val type = object : TypeToken<List<UserApiPojo>>() {}.type
val users = Gson().fromJson<List<UserApiPojo>>(it.data.toString(), type)
offer(users.toMutableList())
} catch (e: java.lang.Exception) {
cancel(CancellationException("API Error", e))
}
}.addOnFailureListener {
cancel(CancellationException("Failure", e))
}
}
}
As you can see, callbackFlow allows us to cancel the flow when something goes wrong and have someone donwnstream handle the error.
Moving to the Repository we would now like to do something like:
val users: Flow<List<User>> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
There are a few caveats here. first() and concat() are operators you will have to come up with it seems. I did not see a version of first() that returns a Flow; it is a terminal operator (Rx used to have a version of first() that returned an Observable, Dan Lew uses it in this post). Flow.concat() does not seem to exist either. The goal of users is to return a Flow that emits the first value emitted by any of the source Flows. Also, note that I am mapping DAO users and Api users to a common User object.
We can now talk about the ViewModel. As I said before, the ViewModel should have something that holds State. This State should represent data, errors and loading states. One way that can be accomplished is with a data class.
data class State(val users: List<User>, val loading: Boolean, val serverError: Boolean)
Since we have access to the Repository the ViewModel can look like:
val state = repo.users.map {users -> State(users, false, false)}.catch {emit(State(emptyList(), false, true)}
Please keep in mind that this is a rough explanation to point you in a direction, there are many ways to accomplish state management and this is by no means a complete implementation. It may not even make sense to turn the API call into a Flow, for example.
The answer from Emmanuel is really close to answering what i need, i need some clarifications about some of it.
It may not even make sense to turn the API call into a Flow
You are totally right, in fact i only want to actually make it a coroutine, i don't really need it to be a flow.
If there is an error with the Flow returned by Room, you can handle it via catch()
Yes i discovered this after posting the question. But my problem is more something like:
I'd like to call a method, say "getData", this method should return the flow from db, start the network call to update the db (so that i'm going to be notified when it's done via the db flow) and somewhere in here, i would need to let the ui know if db or network errored, right?. Or should i maybe do a separate "getDbFlow" and "updateData" and get the errors separately for each one?
val users: Flow> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
This is a good idea, but i'd like to keep the db as the single source of truth, and never return to the ui any data directly from the network

Room with LiveData: Many to Many mapping in adapter

Lets take the following example:
A many to many mapping exists for PRODUCTS and ORDERS. So a product can be on multiple orders and an order can have multiple products. In Room I have an entity which has both the product id and order id as foreign keys so I can save the relations. It's now very easy to get all the orders for a specific product and also all the products for a specific order.
Now here comes the trouble. As far as I know there is no way to get the order object with all of it's products in 1 query/entity. This can be read in further detail in this post. In most places I can bypass this by just running two queries. The first to get the order I'm interested in, and the second to get the products based on the Id of the order.
Now I want to display the combination of an order with its products in an adapter. For that I need to combine all my orders with their products. I'm clueless on how to solve this with LiveData.
The best solution in my opinion would be to create one query that fetches the OrderWithProducts directly from the database. This post suggests it should be possible, but I've not managed to get this to work. Also the most crucial part in that example is missing: the OrderItem class.
If that solution is not possible there must be some way to get the LiveData OrderWithProducts list with 2 queries and somehow combine them.
EDIT
After the suggestions of #Demigod now I have the following in my ViewModel:
// MediatorLiveData can observe other LiveData objects and react on their emissions.
var liveGroupWithLights = MutableLiveData<List<GroupWithLights>>()
fun createOrdersWithProducts() {
appExecutors.diskIO().execute {
val ordersWithProducts = mutableListOf<OrderWithProducts>()
val orders = orderRepository.getGroupsSync()
for (order in orders) {
val products = productRepository.getProductsSync(order.id)
val orderWithProducts = OrderWithProducts(order, products)
ordersWithProducts.add(orderWithProducts)
}
liveGroupWithLights.postValue(ordersWithProducts)
}
}
The function inside my fragment to submit data to the adapter:
private fun initRecyclerView() {
orderListViewModel.getOrdersWithProducts().observe(this, Observer { result ->
adapter.submitList(result)
})
}
So now I'm able to have a OrderWithProduct object as the item for my adapter. This is great, I can use products for each order in my adapter. Now I'm having trouble to update these items whenever the values in the database changes. Any ideas for this part?
Edit2: the invalidationtracker
db.invalidationTracker.addObserver(object : InvalidationTracker.Observer("orders", "products", "order_product_join") {
override fun onInvalidated(tables: MutableSet<String>) {
createOrdersWithProducts()
}
})
The problem I have now is that the validation tracker gets notified a lot for a single change.
As far as I know, it's not possible currently with a single query.
To solve this, you will need to run several queries here. At first - obtain a list of orders with a single query, and after that obtain a list of products per each order. To achieve this, I can think of several options:
Make your own OrdersWithProductsProvider, which will return this combined entities (Order with List<Porduct>), and it will subscribe for the changes to database to emit new objects using LiveData on every orders or products table change.
You can use a MediatorLiveData to fill the list of Orders with their Products, but I don't think this is a best approach since you will need to run query in a background thread, maybe use of Rx is more convenient here.
Personally, I would use a first option, since probably I want to obtain up-to-date list of orders with their products, which means that the update should trigger on change of three tables (products, orders, products_to_orders), which can be done via Room.InvalidationTracker. Inside that provider I would use Rx (which can work with LiveData via LiveDataReactiveStreams).
Addition on how to achieve that:
How to achieve that isn't really matters, the only thing - run this whole query in the background thread post it to LiveData. You can use Executor, Rx, or a simple Thread. So it will look something like:
private val database : Database // Get the DB
private val executor = Executors.newSingleThreadExecutor()
private val liveData = MutableLiveData<List<OrderWithProducts>>()
fun observeOrdersWithProducts():LiveData<List<OrderWithProducts>> {
return liveData
}
private fun updateOrdersWithProducts() {
executor.post {
val ordersWithProducts = mutableListOf<OrderWithProducts>()
val orders = db.orders()
for (order : orders) {
val products = database.productsForOrder(order)
val orderWithProducts = OrderWithProducts(order, products)
ordersWithProducts.add(orderWithProducts)
}
liveData.post(ordersWithProducts)
}
}
Take it as not complete working code, rather an example of implementation.
Call updateOrdersWithProducts on initialization/first call and every time InvalidationTracker will notify about the db change.

Android repository-pattern & RxJava: Use Flowable or Single?

Recently, I´ve read about how important it is to have a Single-Source-Of-Truth (SSOT) when designing an app´s backend (repository, not server-side-backend). https://developer.android.com/topic/libraries/architecture/guide.html
By developing a news-feed app (using the awesome https://newsapi.org/) I am trying to learn more about app architecture.
However, I am unsure of how to design the repository interface for my app.
Btw.: I am using MVVM for my presentation layer. The View subscribes to the ViewModel´s LiveData. The ViewModel subscribes to RxJava streams.
So I came up with 2 approaches:
Approach 1:
interface NewsFeedRepository {
fun loadFeed(): Flowable<List<Article>>
fun refreshFeed(): Completable
fun loadMore(): Completable
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Flowable<List<Article>>
fun moreArticles(): Completable
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Flowable<List<Article>>
fun bookmarkArticle(id: String): Completable
}
This approach is primarily using Flowables which emit data if the corresponding data in the underlying SSOT (database) changes (e.g old data gets replaced with fresh data from API, more data was loaded from API, ...). However, I am unsure if using a Flowable for SearchArticleRepository#searchArticles(...) makes sense. As it is like some request/response thing, where maybe a Single might me be more intuitive.
Approach 2:
interface NewsFeedRepository {
fun loadFeed(): Single<List<Article>>
fun refreshFeed(): Single<List<Article>>
fun loadMore(): Single<List<Article>>
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Single<List<Article>>
fun moreArticles(): Single<List<Article>>​
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Single<List<Article>>
fun bookmarkArticle(id: String): Single<Article> // Returns the article that was modified. Articles are immutable. ​
}
This approach is using Singles instead of Flowables. This seems very intuitive but if the data in the SSOT changes, no changes will be emitted. Instead, a call to the repository has to be made again. Another aspect to take into account is that the ViewModel may have to manage its own state.
Let´s take the FeedViewModel for example (pseudo-code).
class FeedViewModel : ViewModel() {
// Variables, Boilerplate, ...
val newsFeed: LiveData<List<Article>>
private val articles = mutableListOf<Article>()
fun loadNewsFeed() {
// ...
repository.loadFeed()
//...
// On success, clear the feed and append the loaded articles.
.subscribe({articles.clear(); articles.addAll(it)})
// ...
}
fun loadMore() {
// ...
repository.loadMore()
//...
// On success, append the newly loaded articles to the feed.
.subscribe({articles.addAll(it)})
// ...
}
}
So this might not be crucial for a smaller app like mine, but it definitely can get a problem for a larger app (see state management: http://hannesdorfmann.com/android/arch-components-purist).
Finally, I wanted to know which approach to take and why. Are there any best-practices? I know many of you have already done some larger software-projects/apps and it would be really awesome if some of you could share some knowledge with me and others.
Thanks a lot!
I'd rather go for the first approach using Observables instead of Flowables in your case:
interface NewsFeedRepository {
fun loadFeed(): Observable<List<Article>>
fun refreshFeed(): Completable
fun loadMore(): Completable
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Observable<List<Article>>
fun moreArticles(): Completable
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Observable<List<Article>>
fun bookmarkArticle(id: String): Completable
}
I don't see any reason you should necessarily use Flowable for this purpose since you'll never have any OOME related issues checking your repository changes. In other words, for your use case IMHO backpressure is not necessary at all.
Check this official guide which gives us an advice of when to a Flowable over an Observable.
On the other hand, and not related to the question itself, I have serious doubts of what's the purpose of loadMore or moreArticles methods since they return a Completable. Without knowing the context, it may seem you could either refactor the method name by a better name or change the return type if they do what they seem to do by the name.
I believe the first approach is better, Your repo will update the data whenever the data is changed and your view model will be notified automatically and that's cool, while in your second approach you have to call the repo again and that's not really reactive programming.
Also, assume that the data can be changed by something rather than load more event from the view, like when new data added to the server, or some other part of the app changes the data, Now in the first approach again you get the data automatically while for the second your not even know about the changed data and you don't know when to call the method again.

Categories

Resources