Let's assume i' m programming an app showing some kind of countdown.
// somewhere in my fragment:
fun getCountdown(): LiveData<Int> = viewModel.countdown
// 10 ... 9 ... 8 ... etc.
I now want to bind this LiveData to two different TextViews.
<TextView
android:id="#+id/countdownTextView"
android:text="#{fragment.getCountdown}" />
<TextView
android:id="#+id/hurryUpTextView"
android:text="#{fragment.getCountdown}" />
If i only had one of those two views, my BindingAdapter(s) would look like this:
// for the countdown-TextView:
#BindingAdapter("android:text")
fun bindCountdownToTextView(view: TextView, state: LiveData<LoggedInSubState>) {
view.setText("$ Seconds remaining!")
}
// OR for the hurry-up-TextView:
#BindingAdapter("android:text")
fun bindCountdownToTextView(view: TextView, state: LiveData<LoggedInSubState>) {
if(state.value < 3){
view.setText("Hurry up!")
} else {
view.setText("Chill, you have a lot of time")
}
}
Now, what's the most elegant way to use both TextViews/Adapters together?
Should i just create two LiveDatas in my fragment which map countdown to the appropriate string?
Can i somehow specify which adapter i'd like to use?
Should i (try to) write one adapter which internally differenciates between the two views?
Better suggestions?
To make code simple and readable, I'll advise you choose between next approaches:
Use one binding adapter and supply to it already prepared data - your logic particulary will be placed in View or ViewModel.
Use different binding adapter names and post raw data - all work will be placed in each adapter.
What is best suited for you depends on your needs, how common is format logic and , of course, personal opinion.
Related
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.
I am working with overload functions in Kotlin.
In this schematic example, suppose a function whose only difference is the type of view that I pass to the function. One uses TextView, the other uses Button, so I have 2 different functions.
fun workWithViews(v:TextView,...){
// code
}
fun workWithViews(v:Button,...){
// same code!
}
In this case, the properties I use are the same (isAllCaps, gravity, etc.). The problem is that I have to place the same code twice, i.e., the whole code is exactly the same.
It happens because isAllCaps (just like many other properties) it is not a general property of a view, but of some types of views
So it doesn't work, because obviously the compiler see the function parameter, not the real parameter.
I also can make function with a view type parameter, with a big when with my type possibilities:
fun workWithTextView(v:View,...){
when{
(v is TextView) -> {
// code
}
(v is Button) {
// same code
}
} // when
}
The 2 solutions are terrible and generate duplicate code or boilerplate.
I can also do the when before each access to some field, which makes things even worse. Now imagine if one has 5 similar types instead of 2, with many fields in common.
I read some suggestions to create union types in Kotlin. It would be great!
For instance:
fun workWithViews(v:(TextView, Button),...){
// just one code repetition....
}
or
union textBut = TextView , Button
fun workWithViews(v:textBut ,...){
// just one code repetition....
}
In that case I would only have to test a certain type (if (v is typeX)) if I used something specific for that type.
Is there some best solution?
Button is a subclass of TextView, so you can make the function signature take a TextView and put Button-specific stuff in an if-block.
fun workWithTextView(textView: TextView) {
// Do stuff common to TextViews and Buttons.
if (textView is Button) {
// Do extra stuff only for Buttons.
}
}
If Views have common methods, then most likely one view extends the other.
For example, Button extends TextView and you can do this:
fun workWithViews(v:TextView){
// TextView code
}
fun workWithViews(v:Button){
// Button specific code
workWithViews(v as TextView)
}
Using Android Jetpack components and MVVM architecture, we can get live data updates in a View from a View Model in 2 ways, one is to bind the layout with the live data variable, other way is to observe the variable in code.
To illustrate my question I have taken an example. Suppose there is a view model interface getTimeString() which returns the current time.
a) Layout Data Binding
The view in the layout looks something like this
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
...
app:binder_data_date="#{sampleVM.timeString}"/>
The binding adapter looks something like this
#BindingAdapter("binder_data_date")
public static void binder_data_date(TextView text, String data) {
text.setText(data);
}
b) Manual Data binding (just to give it a name):
In Manual data binding, there is nothing in the layout view with respect to the binder, and I observe the live data using the observe() and update the textview.
FragmentSplashScreenManualBindingBinding fragmentSplashScreenBinding;
SampleViewModel sampleViewModel1 = ViewModelProviders.of(this).get(SampleViewModel.class);
public void onSomeRandomFunc(...) {
....
sampleViewModel1.getTimeString().observe(getViewLifecycleOwner(), data -> {
fragmentSplashScreenBinding.sampleText.setText(data);
});
}
I know the first method is much easier to use and both works.
But is using the second method and the way to access the variable (fragmentSplashScreenBinding.sampleText.setText()) in fragment to update the View correct?
Will the performance get impacted if I use the second method?
Your manual Data binding is not incorrect and doesn't have a significant impact on the performance but you will lose two benefits:
Null pointer exception handling: Layout Data Binding handles null data and you don't need to check null objects to prevent app crash when you want to extract data objects and pass them to views.
Code Reusability: If you want to use your layout in different Activities, with
Layout Data Binding you just need to pass the data variable to the layout. But for Manual Data binding you should copy the same code for each java class to assign variable to views which will make a lot of boilerplate code in complex views.
Moreover, If you are using data binding to replace findViewById() as your second method there is a better way called View Binding which you can read more about it here.
Instead of answering your 2 points in post directly - let me mention few key features of both data binding and Live data - which may eventually help you choose 1 over the other.
Live data class supports Transformations - this useful class provide a way to apply any changes to be done to the live data object before dispatching it to the observers, or you may need to return a different LiveData instance based on the value of another one. Below is a sample example of applying the Transformation on LiveData from Official android docs,
class ScheduleViewModel : ViewModel() {
val userName: LiveData
init {
val result = Repository.userName
userName = Transformations.map(result) { result -> result.value }
} }
As you can see from above example - in the "init" the LiveData Object is "transformed" using Transformations.map before dispatching its content to "observers"
Data binding is mostly works with set of Observables and cannot "transform" the data under observation before dispatching like in above example.
Another useful feature of with LiveData is a class called MediatorLiveData - this subclass which may observe other LiveData objects and react based on changes to it - With data binding AFAIK its very much restricted to a specific Observable Fields.
So basically up until now I have been using rxjava2 extensively in the applications, but decided to check out data binding, view models and live data. And Im not sure I've got all of this right, because apart from saving state during rotation of device I do not see any other clear benefits of switching, I could even say that I see downsides of introducing data binding with view model between view and rx java powered requests.
Lets see example of some registration form. It would contain:
2 inputs - name and surname
Field with 3 choices
Button with progress
In the reactive world I would have two observables with name and surname, one observable that would merge 3 choices clicks and map them to the right enum, then I could combine all the data together, communicate directly with my single responsible for sending the data in between I would have state with progress or error and tada Im done.
And here is the thing that I came up with using data binding and view models:
class LiveDataViewModel : ViewModel() {
enum class Choice {
NONE, FIRST, SECOND, THIRD
}
private val _progressVisibilityLiveData = MutableLiveData<Boolean>()
private val _errorLiveData = MutableLiveData<GlobalError>()
val progressVisibilityLiveData: LiveData<Boolean> = _progressVisibilityLiveData.apply { value = false }
val errorLiveData: LiveData<GlobalError> = _errorLiveData
val data = LiveDataData()
val observableData = ObservableField(LiveDataData())
fun actionContinue() {
_progressVisibilityLiveData.postValue(true)
if (observableData.get()?.isValid() == false) _errorLiveData.postValue(GlobalError.AllFieldsRequired)
else sendToApi()
}
private fun sendToApi() {
// TODO there would be still an rx java call to single, when we would handle error in the same way we are doing
// it in actionContinue
}
data class LiveDataData(val firstName: ObservableField<String> = ObservableField(""),
val secondName: ObservableField<String> = ObservableField(""),
val choice: ObservableField<Choice> = ObservableField(Choice.NONE)) {
fun changeChoice(newChoice: Choice) {
choice.set(newChoice)
}
fun isValid(): Boolean = !firstName.get().isNullOrEmpty() && !secondName.get().isNullOrEmpty() && choice.get() != Choice.NONE
fun toRequest(): Request = Request(firstName.get()!!, secondName.get()!!, choice.get()!!)
}
}
So I would change fields of my LiveDataData directly from xml using bindData, also I would change state of my selection box depending on this binding too, progress would have to be done manually and then it would trigger the visibility using data binding. But is it really a good way of handling such cases?
The disadvantages I see are that the whole logic in actionContinue would be manually changing values, the values from ObservableProperties could be null, so we either have to handle nullable values everywhere of we have to use !! and to be honest Im not feeling that this is the right direction.
Maybe any of you guys have thought about something similar and could eventually point me if I made some wrong assumptions or if I shouldn't use for example ObservableProperty at all. Obviously there are tons of articles about data binding and live data etc, but I haven't found any that would satisfy my curiosity. Oh and create MutableLiveData for each property from form is not an option.
RxJava is a completely different concept than DataBinding. It's more of a way of handling concurrency than it is about binding data. I 100% think it's worth learning. The Android community has embraced it with open arms.
Shameless plug: I compiled a list of RxJava resources awhile back - http://gregloesch.com/dev/2014/10/20/resources-for-learning-rxjava-android.html
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