I am converting my application to room database and try to follow the google architecture best practices based on "Room with a View".
I am having trouble to understand the repository in terms of clean architecture.
The Words database example contains only one table and one view using it, making it a simple HelloWorld example. But lets start with that.
There is a view which displays a list of words. Thus all words need to be read from the database and displayed.
So we have a MainActivity and a Database to connect.
Entity Word
WordDao to access DB
WordViewModel: To separate the activity lifecycle from the data lifecycle a ViewModel is used.
WordRepository: Since the data maybe kept in a database or the cloud or whatever the repository is introduced to handle decision, where data comes from.
Activity with the View
It would be nice if the view is updated when the data changes, so LiveData is used.
This in turn means, the repository is providing the LiveData for the full table:
// LiveData gives us updated words when they change.
val allWords: LiveData<List<Word>>
This is all fine for a single view.
Now to my questions on expanding this concept.
Let us assume, the word table has two columns "word" and "last_updated" as time string.
For easier comparison the time string needs to be converted to milliseconds, so I have a function.
Question: Where to put the fun queryMaxServerDateMS() to get the max(last_updated)?
/**
* #return Highest server date in table in milliseconds or 1 on empty/error.
*/
fun queryMaxServerDateMS(): Long {
val maxDateTime = wordDao.queryMaxServerDate()
var timeMS: Long = 0
if (maxDateTime != null) {
timeMS = parseDateToMillisOrZero_UTC(maxDateTime)
}
return if (timeMS <= 0) 1 else timeMS
}
For me it would be natural to put this into the WordRepository.
Second requirement: Background job to update the word list in the database.
Suppose I now want a Background Job scheduled on a regular basis which checks the server, if new entries were made and downloads them to the database. The app may not be open.
This question just relays to the question of the above queryMaxServerDateMS.
The job will basically check first, if a new entry was made by asking the server if an entry exists which is newer then the max known entry.
So I would need to get a new class WordRepository, do my query, get max last_update and ask the server.
BUT: I do not need the LiveData in the background job and when val repositoy = WordRepository the full table is read, which is needless and time-, memory and batteryconsuming.
I also can think of a number of different fragments that would require some data of the word table, but never the full data, think of a product detail screen which lists one product.
So I can move it out to another Repository or DbHelper however you want to call it.
But in the end I wonder, if I use LiveData, which requires the View, ViewModel and Repository to be closely coupled together:
Question: Do I need a repository for every activity/fragment instead of having a repository for every table which would be much more logical?
Yes, with your current architecture you should put it in the Repository.
No, you don't need a repository for every activity/fragment. Preferably, 1 repository should be created for 1 entity. You can have a UseCase for every ViewModel.
In Clean architecture there's a concept of UseCase / Interactor, that can contain business logic, and in Android it can act as an additional layer between ViewModel and Repository, you can create some UseCase class for your function queryMaxServerDateMS(), put it there and call it from any ViewModel you need.
Also you can get your LiveData value synchronously, by calling getValue().
You do not need repository for each activity or fragment. To answer your question about getting max server time - when you load words from db you pretty much have access to entire table. That means you can either do that computation yourself to decide which is the latest word that's added or you can delegate that work to room by adding another query in dao and access it in your repo. I'd prefer latter just for the simplicity of it.
To answer your question about using repo across different activities or fragment - room caches your computations so that they are available for use across different users of your repo (and eventually dao). This means if you have already computed the max server time in one activity and used it there, other lifecycle owners can use that computed result as far as the table has not been altered (there might be other conditions as well)
To summarize you're right about having repository for tables as opposed to activities or fragments
Related
Should i update my User Details in Viewmodel as well as SharedPrefernces?
For example, i have fields like
name
age
currentSalary
organisation
+8 more
I have few doubts now:
Am i supposed to create LiveData of each of these fields?
Also, i have to save them in Sharedprefernces too. So doesn't this feel redundant? First, saving it in Viewmodel and then saving it in Sharedprefernces.
When i move from screen 1 to screen 2, should i fetch user's name from ViewModel or the api?
MainViewModel.kt Sample Code
private val _experience = MutableLiveData<String>()
val experience : LiveData<String>
get() = _experience
private val _name = MutableLiveData<String>()
val name : LiveData<String>
get() = _name
private val _isLoggedIn = MutableLiveData<Boolean>()
val isLoggedIn : LiveData<Boolean>
get() = _isLoggedIn
fun setName(name: String){
_name.value = name
}
fun setExperience(exp: String){
_experience.value = exp
}
fun logIn(){
_isLoggedIn.value = true
}
MyFragment
binding.btnSubmit.setOnClickListener {
val name = binding.etName.text.toString()
val email = binding.etEmail.text.toString()
val age = binding.etAge.text.toString()
val org = binding.etOrg.text.toString()
//saving in viewmodel
mainViewModel.setName(name)
mainViewModel.setCurrentOrganisation(org)
mainViewModel.setEmail(email)
mainViewModel.setAge(Integer.valueOf(age))
//saving in sharedpreference
editor.putInt("age", Integer.valueOf(age))
editor.putString("name", name)
editor.putString("email", email)
editor.putString("org", org)
}
To me, My fragment looks a lot of lines of codes. I don't know if i am using the right approach.
The ViewModel is meant to sit between your View layer (the UI) and the Data layer (the core app functionality, stored data etc). A ViewModel acts as a go-between, passing data to the UI for display, and translating UI events to function calls in the app.
The ViewModel's state is transient - in Android it sticks around long enough to survive things like Activity recreation. If you use the SavedStateHandle component, you can store its running state so it can be rebuilt if the app is destroyed in the background - but this explicitly won't survive the app being closed and restarted. The ViewModel isn't about persisting data, just about the current, temporary state of things.
So actually storing your data is part of the data layer. That's where the SharedPreferences comes in - but you could be storing (and reading) data using all kinds of storage, even across a network. The ViewModel's role is to access that data, and expose it to the UI somehow - possibly even transforming that data into a more suitable form for the UI to consume.
So while you might have duplication going on, there's a reason for it. SharedPreferences is there to actually store and persist the data. LiveData is just there for the UI to see what it should be displaying. They may or may not be the same thing!
Have a look at this Android guide on designing your app architecture - it goes into the theory behind how you organise things, and how the data flow works. You don't necessarily need to follow all of it, but the broad strokes are good to know - that way if you want to deviate from that for simplicity in a particular situation, you'll know why you're doing it and what compromises you're making (and whether they matter in this case).
For your data update... there's two ways you can go about it. One is to make the ViewModel (gonna say VM for brevity) update the data layer, and then have the data layer push new data to the VM, which the VM displays in its LiveData. This is the kind of thing you do when you're using observable queries with a database, where updates to a table push new data to the observer.
All the VM has to worry about is pushing data to the data layer (e.g. calling a delete item function). When the data changes, it's pushed to the observing VM, which just displays the data as usual (e.g. setting it on a LiveData), which causes the UI to get an update and display the new state... So instead of the VM getting the delete event, and having to worry about updating its own internal state, it just passes the delete request to the data layer. Then the new state arrives later, and the VM just uses that - a new list, whatever. The data layer tells the view model what to display, in the same way the VM tells the UI what to display.
(It might not be worth the effort writing a thing that updates the SharedPreferences and then tells the VM to display that data, so you could just do all this in the VM as a kind of combination of VM and data layer - but it helps to know why you're doing it, what it's a shortcut for, y'know?)
As for the "lots of LiveData objects" bit, have a look at this section on UI State. Basically, the officially recommended approach is that the the UI's state as a whole is pushed by the VM. So instead of separate LiveData objects for each property (name, age etc) you'd have a single object that contains that data, and a LiveData that pushes that. Whenever something about that data changes, you push a new instance of that data object. (Data classes can help here, with their copy functions that let you change specific values and keep the rest)
So your UI just observes that one state LiveData, and wires it up to the UI components - your TextViews, CheckBoxes etc. The approach they're talking about isn't just data - it's also UI state, which may not be what you're storing in your actual data layer (e.g. if a particular section is expanded, or if some list items are checked for a potential delete operation). Exactly how much you want to encapsulate in one object is up to you - but pushing your actual data in a single data structure isn't a bad idea!
Sorry that was a bit long, but hopefully it helps a bit
I am learning Android Architecture Components.
For exemple, and be more easier to understand, If i want to build a TO DO LIST app, my item creation DAO should be
#Dao
public interface ItemDao {
#Insert
long insertItem(Item item);
}
and my viewModel could be use this DAO to insert an item in my TODO list.
But, in architecture component, it is recommanded to NOT manipulate the database by the viewmodel but by the repository.
So, the code should be like that
public class ItemDataRepository {
private final ItemDao itemDao;
public ItemDataRepository(ItemDao itemDao) { this.itemDao = itemDao; }
// --- CREATE ---
public void createItem(Item item){ itemDao.insertItem(item); }
It seems redundant when we cannot understand why.
My question is : why?
I use the Repository for a couple of reasons:
Separation of concern I let the repo be responsible for downloading and storing all the data. That way the ViewModel doesn't have to know any specifics about where the data is coming from, e.g. if it's from an API or a cache. It also makes it easier to write Unit tests for the ViewModel, since all the database and API logic should already be tested in Unit tests for the Repository.
Reusability Lets say you fetch the data from an API and store in a database. If you put the code in the ViewModel and then want to perform the same actions from another place in the app, you need to copy paste the code. By having it in a Repository you can easily share the implementation.
Share data If you have multiple screens that show parts of the same data set, having one Repository passed around between the screens make it easy to share the data. No more trying to pass large amount of data in Bundle and Intent.
Lifecycle Let's say you download data from an API to show in your view. If you fetch the data in the ViewModel you will have to re-download it when the screen is closed and re-opened, since the ViewModel is discarded. However if you store it in the Repository it can use the Application lifecycle, so when you revisit the screen again, the data is already in the cache.
maybe the headline does not fit the question really well so I'll explain it.
In my app I send a request to a server to get gerneral user information. If I receive on I want to save it into the local room db. Now we come to the problem. I want to bind this one user to the view directly out of the db. But I can not bind a element, which maybe does not exists, because the request is in progress.
(My bad solution): Creating another livedate element which holds a boolean. I create a observer in the Activity and add the observer after the boolean observes a "true". With this solution I can not use "Data Binding" in the xml layout.
Does anyone have an idea? (If you need further information just ask - I know it is a really abstract question without any code)
As Sanlok Lee mentioned:
If you reassign user then it becomes a completely different instance and the observer will not listen to the new LiveData. Instead you can do val user: MediatorLiveData<User> and later you can call user.addSource(dao.getUserById(1), ...)
OR:
Just load the user (also if you know that there is no one in the DB) from the DB. You can do that in you UserRepository for example.
val user: LiveData<User> = userDao.getUser()
The Livedata will get notified when there is a valid user inserted.
I have a social networking app which displays a list of users, and am looking to have an efficient way of being able to retrieve an object from my LiveData using its primary key.
Example: Retrieve a set of User POJOs from within my LiveData<List<User>> given a LIST of userId Integers (ie, users 12, 5, 7, and 1). I need to be able to look up these users by the userId for display in the appropriate order in the UI.
I believe I want something more like LiveData<Map<Integer, User>>, but how could I implement this using the Room database, without breaking the LiveData callbacks from my local DB -> Room -> LiveData -> UI?
PROPOSAL 1:
Change my Room implementation to somehow return a LiveData containing a HashMap of <userId,User>.
Current Room implementation:
#Query("SELECT * FROM users WHERE user_id in :userIds LIMIT 1")
LiveData<List<User>> getUsers(List<Integer> userIds);
Proposed Room implementation (no idea if something like this is possible or what it would even look like):
#Query("SELECT * FROM users WHERE user_id in :userIds LIMIT 1")
LiveData<**HashMap**<Integer,User>> getUsers(List<Integer> userIds);
PROPOSAL 2:
Have a list of many LiveData objects WITHIN a Map:
Map<Integer,LiveData<User>> liveDataUsers;
This might be something to look into, but I'm worried that having potentially hundreds/thousands of LiveData objects within a map is bad design and could also lead to performance issues / too many open LiveData internal callback threads.
PROPOSAL 3:
Something else??? I feel like I am missing something easy here. How are others looking up objects within their LiveData using only their primaryKey?
edit: this is something I'd like to achieve at the Repo / Model level and not at the activity level, as this LiveData will be re-used throughout the app.
I have been implementing the new Paging Library with a RecyclerView with an app built on top of the Architecture Components.
The data to fill the list is obtained from the Room database. In fact, it is fetched from the network, stored on the local database and provided to the list.
In order to provide the necessary data to build the list, I have implemented my own custom PageKeyedDataSource. Everything works as expected except for one little detail. Once the list is displayed, if any change occurs to the data of a list's row element, it is not automatically updated. So, if for example my list is showing a list of items which have a field name, and suddenly, this field is updated in the local Room database for a certain row item, the list does not update the row UI automatically.
This behaviour only happens when using a custom DataSource unlike when the DataSource is obtained automatically from the DAO, by returning a DataSource Factory directly. However, I need to implement a custom DataSource.
I know it could be updated by calling the invalidate() method on the DataSource to rebuild the updated list. However, if the app is showing 2 lists at a time (half screen each for example), and this item appears in both lists, it would be needed to call invalidate() for both lists separately.
I have thought with a solution in which, instead of using an instance of the item's class to fill each ViewHolder, it uses a LiveData wrapped version of it, to make each row observe for changes on its own item and update that row UI when necessary. Nevertheless, I see some downsides on this approach:
A LifeCycleOwner (such as the Fragment containing the RecyclerView for example) must be passed to the PagedListAdapter and then forward it to the ViewHolder in order to observe the LiveData wrapped item.
A new observer will be registered for each list's new row, so I do not know at all if it has an excessive computational and memory cost, considering it would be done for every list in the app, which has a lot of lists in it.
As the LifeCycleOwner observing the LiveData wrapped item would be, for example, the Fragment containing the RecyclerView, instead of the ViewHolder itself, the observer will be notified every time a change on that item occurs, even if the row containing that item is not even visible at that moment because the list has been scrolled, which seems to me like a waste of resources that could increase the computational cost unnecessarily.
I do not know at all if, even considering those downsides, it could seem like a decent approach or, maybe, if any of you know any other cleaner and better way to manage it.
Thank you in advance.
Quite some time since last checked this question, but for anyone interested, here is the cause of my issue + a library I made to observe LiveData properly from a ViewHolder (to avoid having to use the workaround explained in the question).
My specific issue was due to a bad use of Kotlin's Data Classes. When using them, it is important to note that (as explained in the docs), the toString(), equals(), hashCode() and copy() will only take into account all those properties declared in the class' constructor, ignoring those declared in the class' body. A simple example:
data class MyClass1(val prop: Int, val name: String) {}
data class MyClass2(val prop: Int) {
var name: String = ""
}
fun main() {
val a = MyClass1(1, "a")
val b = MyClass1(1, "b")
println(a == b) //False :) -> a.name != b.name
val c = MyClass2(2)
c.name = "c"
val d = MyClass2(2)
d.name = "d"
println(c == d) //True!! :O -> But c.name != d.name
}
This is specially important when implementing the PagedListAdapter's DiffCallback, as if we are in a example's MyClass2 like scenario, no matter how many times we update the name field in our Room database, as the DiffCallback's areContentsTheSame() method is probably always going to return true, making the list never update on that change.
If the reason explained above is not the reason of your issue, or you just want to be able to observe LiveData instances properly from a ViewHolder, I developed a small library which provides a Lifecycle to any ViewHolder, making it able to observe LiveData instances the proper way (instead of having to use the workaround explained in the question).
https://github.com/Sarquella/LifecycleCells