I use LiveData and databinding in my app to populate a recyclerview from the viewmodel.
The property holding the items is defined as
abstract val searchItems : LiveData<List<BindableItem<*>>>
However, databinding is stripping the type for the LiveData and generating an Object type live data, which fails to compile.
This is the generated code for the property above:
android.arch.lifecycle.LiveData searchViewModelSearchItems = null;
For other non-generics properties the type is retained, for instance
android.arch.lifecycle.LiveData<java.util.List<com.example.feature.search.adapter.CarouselItem>> searchViewModelCarouselItems = null;
How can use generics with databinding?
I have not found a way to get the databinding compiler to accept a list of generics, but a workaround is to define the list to be of type Any and then cast to List<BindableItem<*>> in the binding adapter that adds the items to the recyclerview. Not ideal, but at least works.
In the viewmodel:
abstract val items: LiveData<List<Any>>
In the binding adapter:
fun setItems(
recyclerView: RecyclerView,
items: List<Any>?,
onItemClickListener: OnItemClickListener?
) {
val bindableItems = items as? List<BindableItem<*>>
/* do other stuff */
}
Related
I'm using data-binding to handle MutableStateFlow in my ViewModel
private val _searchSkeleton = MutableStateFlow(View.GONE)
val searchSkeletonVis = _searchSkeleton
Here's my usage in the XML file
app:viewVisibility="#{viewModel.searchSkeletonVis}"
And this is my BindingAdapter
#BindingAdapter("app:viewVisibility")
fun bindViewVisibility(
#NonNull view: View,
#NonNull visibility: Int,
) {
view.visibility = visibility
}
and it gives me this error
Cannot find a setter for that accepts parameter type 'kotlinx.coroutines.flow.MutableStateFlow'
But when I use LiveData it's working fine!
Stateflow is only supported in Android Gradle Plugin version 7.0.0-alpha04 or higher as per the documentation.
have this code, in viewModel (kotlin)
private val _shoeData = MutableLiveData<List<Shoe>>()
val shoeData : LiveData<List<Shoe>>
get() {
return _shoeData
}
how two-data-binding can be implemented in a View (e.g: Fragment), like below wouldn't work
android:text="#={shoeStoreViewModel.shoeData.name}"
as shoeData is now a list
Question
I'm looking to refactor an immutable view state's values in the Android ViewModel (VM) in order to do the following:
Update the view state in the VM cleanly without copying the entire view state
Keep the view state data immutable to the view observing updates
I've built an Android Unidirectional Data Flow (UDF) pattern using LiveData to update the view state changes in the VM that are observed in the view.
See: Android Unidirectional Data Flow with LiveData — 2.0
Full sample code: Coinverse Open App
Implementation
The existing implementation uses nested LiveData.
One LiveData val to store the view state in the VM
Nested LiveData for the view state attributes as immutable vals
// Stored as viewState LiveData val in VM
data class FeedViewState(
val contentList: LiveData<PagedList<Content>>
val anotherAttribute: LiveData<Int>)
The view state is created in the VM's init{...}.
Then, in order to update the view state it must be copied and updated with the given attribute because it is an immutable val. If the attribute were to be mutable, it could be reassigned cleanly without the copy in the VM. However, being immutable is important to make sure the view cannot unintentionally change the val.
class ViewModel: ViewModel() {
val viewState: LiveData<FeedViewState> get() = _viewState
private val _viewState = MutableLiveData<FeedViewState>()
init {
_viewState.value = FeedViewState(
contentList = getContentList(...)
anotherAttribute = ...)
}
override fun swipeToRefresh(event: SwipeToRefresh) {
_viewState.value = _viewState.value?.copy(contentList = getContentList(...))
}
}
I am not sure if having "nested LiveData" is okay. When we work with any event-driven design implementation (LiveData, RxJava, Flow) we usually required to assume that the discrete data events are immutable and operations on these events are purely functional. Being immutable is NOT synonymous with being read-only(val). Immutable means immutable. It should be time-invariant and should work exactly the same way under any circumstances. That is one reason why I feel strange to have LiveData or ArrayList members in the data class, regardless of whether they are defined read-only or not.
Another, technical reason why one should avoid nested streams: it is almost impossible to observe them correctly. Every time there is a new data event emitted through the outer stream, the developers must make sure to remove inner subscriptions before observing the new inner stream, otherwise it can cause all sorts of problems. What's the point of having life-cycle aware observers, when the developers need to manually unsubscribe them?
In almost all scenarios, nested streams can be converted to one layer of stream. In your case:
class ViewModel: ViewModel() {
val contentList: LiveData<PagedList<Content>>
val anotherAttribute: LiveData<Int>
private val swipeToRefreshTrigger = MutableLiveData<Boolean>(true)
init {
contentList = Transformations.switchMap(swipeToRefreshTrigger) {
getContentList(...)
}
anotherAttribute = ...
}
override fun swipeToRefresh(event: SwipeToRefresh) {
swipeToRefreshTrigger.postValue(true)
}
}
Notes on PagedList:
PagedList is also mutable, but I guess it is something we just have to live with. PagedList usage is another topic so I won't be discussing it here.
Use Kotlin StateFlow - 7/21/20 Update
Rather than having two state classes with LiveData, one private and mutable, the other public and immutable, with the Kotlin coroutines 1.3.6 release a StateFlow value can be updated in the ViewModel, and rendered in the view's activity/fragment through an interface method.
See: Android Model-View-Intent with Kotlin Flow
Remove Nested LiveData, Create State Classes - 2/11/20
Approach: Store immutable LiveData state and effects in a view state and view effect class inside the ViewModel that is publicly accessible.
The view state and view effects attributes could be LiveData values directly in the VM. However, I'd like to organize the view state and effects into separate classes in order for the view to know whether it observing a view state or a view effect.
class FeedViewState(
_contentList: MutableLiveData<PagedList<Content>>,
_anotherAttribute: MutableLiveData<Int>
) {
val contentList: LiveData<PagedList<Content>> = _contentList
val anotherAttribute: LiveData<Int> = _anotherAttribute
}
The view state is created in the VM.
class ViewModel: ViewModel() {
val feedViewState: FeedViewState
private val _contentList = MutableLiveData<PagedList<Content>>()
private val _anotherAttribute = MutableLiveData<Int>()
init {
feedViewState = FeedViewState(_contentList, _anotherAttribute)
}
...
fun updateContent(){
_contentList.value = ...
}
fun updateAnotherAttribute(){
_anotherAttribute.value = ...
}
}
Then, the view state attributes would be observed in the activity/fragment.
class Fragment: Fragment() {
private fun observeViewState() {
feedViewModel.feedViewState.contentList(viewLifecycleOwner){ pagedList: PagedList<Content> ->
adapter.submitList(pagedList)
}
feedViewModel.feedViewState.anotherAttribute(viewLifecycleOwner){ anotherAttribute: Int ->
//TODO: Do something with other attribute.
}
}
}
I'm trying out databinding for a view that's supposed to display data exposed through a LiveData property in a viewmodel, but I've found no way to bind the object inside the LiveData to the view. From the XML I only have access to the value property of the LiveData instance, but not the object inside it. Am I missing something or isn't that possible?
My ViewModel:
class TaskViewModel #Inject
internal constructor(private val taskInteractor: taskInteractor)
: ViewModel(), TaskContract.ViewModel {
override val selected = MutableLiveData<Task>()
val task: LiveData<Task> = Transformations.switchMap(
selected
) { item ->
taskInteractor
.getTaskLiveData(item.task.UID)
}
... left out for breivety ...
}
I'm trying to bind the values of the task object inside my view, but when trying to set the values of my task inside my view I can only do android:text="#={viewmodel.task.value}". I have no access to the fields of my task object. What's the trick to extract the values of your object inside a LiveData object?
My task class:
#Entity(tableName = "tasks")
data class Task(val id: String,
val title: String,
val description: String?,
created: Date,
updated: Date,
assigned: String?)
For LiveData to work with Android Data Binding, you have to set the LifecycleOwner for the binding
binding.setLifecycleOwner(this)
and use the LiveData as if it was an ObservableField
android:text="#{viewmodel.task}"
For this to work, Task needs to implement CharSequence. Using viewmodel.task.toString() might work as well. To implement a two-way-binding, you'd have to use MutableLiveData instead.
why are you using two way binding for TextView
android:text="#={viewmodel.task.value}"
instead use like this android:text="#{viewmodel.task.title}"
if I have a class
data class item(val address: String = ""
)
its declared in my viewmodel
var varLive: MutableLiveData = MutableLiveData()
and later on I post it from my viewmodel
varLive.postValue(scootersList[marker])
in my xml I have
<TextView
...
android:text="#{vModel.varLive.address}"
/>
And I can't access item.address and get a databinding error.
I can check if the varLive is null and tht is it
Do I really have to declare each of the livedata class fields as a live data? If I have a class holding 100 members?
for some stupid reason you have to specify a getter method in your viewmodel, so databinding can pick it up. like so:
fun getvarLive() = varLive
Kotlin actially does that for you. But databinding won't bind Kotlin getters. Seriosly annoying