Passing Data Class To Other Composable Screen - android

i have a data class
data class Holder(uri: String, title: String, desc: String, source: String, color: String?) that i have from screen a, that i want to pass along to screen b. i have the code set up as follows:
#Composable
fun A(navigateToScreenB: (holder: Holder) -> Unit) {...}
#Composable
fun B(holder: Holder) {...}
in my nav file:
composable(
route = Screen.b.route
) {
B(how to get `Holder` to pass here)
}
composable(
route = Screen.A.route
) {
A(
navigateToScreenB = { it ->
// `it` is my data class `Holder`, but how to pass it to screen b?
navController.navigate(Screen.B.route)
}
)
}
any insight on this?

tl/dr: you shouldn't be passing the object at all, the same way you don't see a website that is example.com/data/{the whole object in the URL}, you'd see example.com/data/{a unique ID used to retrieve the object}
As per this thread, if the object exists only at the UI layer (i.e., it isn't coming from a remote database or any other source of truth), you can just pass the object directly by following the documentation and making your class Parcelable and serializing it to include it as part of your Screen.b.route just like any other argument.
However, you've said that your Holder object actually comes from a remote database. That same thread goes on to discuss exactly this case:
A better way to start the conversation is figuring out how StoreList is going to recreate its list after process death and recreation. Your objects have been wiped from memory entirely, which means that ViewModel needs to be talking to the actual source of truth (i.e., your repository layer - note that doesn't mean "save to disk" or "database", it just means the layer responsible for getting your data)
Once you have a repository layer anyways (since a ViewModel shouldn't be doing network calls or disk access directly), that's also the layer that can do in memory caching
Keep in mind that every list already has a way to uniquely define each item by the index in the list
So at that point you already have everything you need - a repository both destinations can talk to that is the single source of truth for Store objects and an index into the list that you can pass to the details screen to define you element
And if you start persisting those objects into a database, the rest of the layers don't have to care
(of course, you probably will add a unique ID to each element if you do add it to a database at some point, which then makes the index based approach unnecessary)
So how you should actually be doing this is:
Create a data layer that is responsible for loading your data. No other layer should know that your data is actually from a remote database. Okhttp caching is a simple way to add caching to prevent redownloading data in a way that is transparent to the other layers.
Both Screen A and Screen B would talk to the same data layer, generally, as per the UI Layer documentation, would be by using a state holder such as a ViewModel that is responsible for loading data from the data layer.
2a) Screen A would request the entire list of Holder objects from the data layer, which would load the entire list from the remote database.
2b) Screen B would request just a single Holder object, either by using the index in the list or a unique key if one is available (it isn't clear from your Holder class if a unique key exists)
Instead of passing the entire Holder as part of Screen B's route argument, you'd pass only that unique key, which could simply be the index in the list or something more complicated if your remote database has such a unique key (note that a Uri would need to be encoded via Uri.encode if included in a route)
This approach follows the approach of a single source of truth. If you later change your data layer to store data locally, none of the rest of your layers need to change. If Screen B gains the ability to edit your Holder objects, then Screen A (assuming your data layer uses a Flow or similar observable structure) would automatically update, without needing to pass data back or anything complicated like that.
It also means that cases which are generally hard to handle, such as configuration changes (i.e., rotating your device) or process death and recreation (which can happen at any screen, meaning you can't rely on Screen A to have loaded your data into memory) are also handled without any additional work.

Related

Should i save my User Details in Viewmodel as well as SharedPrefernces?

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

Paging 3 RemoteMediator and SavedStateHandle

I'm using the RemoteMediator in an app to load page keyed data. Everything works fine, except when after process death, the data is refreshed.
My current implementation is :
val results = savedStateHandle.get<String>("query").flatMapLatest { query ->
repository.getPager(
query = query,
)
}.cachedIn(viewModelScope)
I do know about the initialize() function of RemoteMediator, but how do I tie it in with process death?
As you found out, .cachedIn just operates in memory, so it won't survive process death. You cannot rely on Paging's internal cache of items in memory for this, you need to cache the loaded items on disk.
I would recommend using something like Room or some dedicated persistence layer that is actually built to handle large lists of arbitrary data classes.
I would not recommend to try to serialize and stash the entire list of data into SavedState as this could become prohibitively expensive quite quickly.
For your other point on RemoteMediator - it is just a "dumb" callback which has no influence on what Paging actually loads or displays. It's simply a way for your to write custom logic which is triggered during edge-case conditions in Paging. You probably only want this if you are already using a layered approach and trying to skip remote REFRESH. If that is your case, the RemoteMediator.intiailize function is guaranteed to complete before Paging starts loading, which means you can check whether you are coming from SavedState and there is already cached data, and if so, you can skip remote REFRESH by returning InitializeAction.SKIP_INITIAL_REFRESH.

Best way to enhance PagedList with UI-specific data and not break MVVM pattern

I'm new to MVVM pattern, but I really liked how PagedList simplifies working with paged data in repository.
But now I have the following situation:
I have repository with method like this:
fun getLibraryItems(): LiveData<PagedList<ItemInfo>>
Where ItemInfo is Repository-specific and doesn't 'know' about UI.
Next I want to 'enhance' this data object with UI specific data (map to another ui data object), which I can get only from context-aware components, f.e. it's Drawable resource.
But I cannot do 'map' straight forward from PagedList<X> to another PagedList<Y>, if I want it - I need to update my data source to accept 'mapper' function, like this:
fun <T> getLibraryItems(mapper: (ItemInfo) -> T): LiveData<PagedList<T>> {
return dataSource
.getLibraryItems()
.map(mapper)
.toLiveData()
}
This way I can 'map' ItemInfo to UI-specific type T, but I cannot do it in ViewModel, because loading Drawable resources in ViewModel is (as I understand) anti-pattern.
I don't understand how and where should I call this 'mapper', should it be Activity/Fragment or am I missing smth and over-complicating things.
I think it's possible that you map the data inside the ViewModel as you've suggested, but instead of loading Drawable resource right there you pass its identifier only. And then in your UI layer (RecyclerView Adapter,Activity or whatever) load the resource by its identifier. This way you won't have to deal with context-specific methods inside ViewModel. Also if you don't wan't to deal with android resource id's (e.g. R.drawable.my_image) you'll have to keep your own ID system and then map it to android resource id inside UI layer, though I personally consider this redundant complication.
Edit: After re-reading your question I see that my post didn't fully answer it, so here's a bit more on architecture:
Since you want to map a domain object to UI-specific object, you're right that doing so inside a ViewModel isn't the best practice and not a clean architecture way. So the right way would be to do such mappings in your UI layer just before you display the object. Since you work with PagedList I can assume your RecyclerView adapter extends PagedListAdapter. Then you can create an abstraction for supporting mappings inside an adapter.
abstract class MyPagedAdapter<T, R, VH: RecyclerView.ViewHolder>(
val dataMapper: (T)->R,
diffCallback: DiffUtil.ItemCallback<T>
): PagedListAdapter<T, VH>(diffCallback) {
fun getMappedItem(position: Int) = dataMapper.invoke(getItem(position))
}
class LibraryItemAdapter(
dataMapper: (LibraryItem)->UILibraryItem,
): MyPagedAdapter<LibraryItem, UILibraryItem, ViewHolderType>(dataMapper, /* provide diff callback here */) {
/* implement onCreateViewHolder and getItemViewType here */
override fun onBindViewHolder(holder: ViewHolderType, position: Int) {
val item = getMappedItem(position)
// configure view holder to display selected item
}
}
// Then inside your fragment/activity:
val mapper: (LibraryItem) -> UILibraryItem = { /* use your context-aware components to create an object for UI */ }
recyclerView.adapter = LibraryItemAdapter(mapper)
P.S: I'd also like to highlight that mapping the whole list inside your ViewModel/Repository/elsewhere to another class which holds a Drawable, Bitmap or any other large structure is a bad idea, you could end up flooding memory with Drawables which may be not even displayed at the moment. Get your drawables only when you're going to use them and don't store them in a list. My initial answer and later edit both keep you from doing so
P.P.S.: Don't put too much work into your mapper as it may load the UI thread a lot when user scrolls a large list quickly. Do all your business logic in domain/repository layer.

Android architecture LiveData and Repositories

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

Paging Library with custom DataSource not updating row on Room update

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

Categories

Resources