I have two variables inside my layout file :
<data>
<variable name="createExpenseViewModel" type="com.lionosur.dailyexpenses.viewModels.MainViewModel"/>
<variable name="createExpenseConverter" type="com.lionosur.dailyexpenses.converters.createExpenseActivityConverter.Companion"/>
</data>
My view model has an method to return the live data :
fun getAllExpenseItems(): LiveData<List<Expense>> {
return expenseRepository.getAllExpenseItems()
}
I need to observe this data and populate an spinner,
class createExpenseActivityConverter {
// contains all the static methods to convert the data for the ui
companion object {
fun getExpenseCategoryListFromSource(list:List<Source>):ArrayList<String> {
val categoryItems = ArrayList<String>()
categoryItems.addAll(list.map { it.sourceName })
return categoryItems
}
}
}
to populate a spinner I need to supply an array list of string
<Spinner
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="#+id/expense_category"
android:entries="#{()-> createExpenseViewModel.getAllSourceItems(1) }"
app:layout_constraintStart_toStartOf="#+id/textView"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="#+id/textView" app:layout_constraintWidth_percent="0.7"
/>
in android:entries I need to convert the observed data to array list of string, how do I pass the #{()-> createExpenseViewModel.getAllSourceItems(1) } result in to another static method createExpenseViewConverter.getExpenseCategoryListFromSource(sourceList) which would return a array list of string.
in my activity i have setup binding like this
binding = DataBindingUtil.setContentView(this, R.layout.activity_create_expense)
val mainViewModel = DaggerExpenseComponent.builder()
.setContext(this)
.build()
.getExpenseViewModel()
binding.setLifecycleOwner(this)
binding.createExpenseViewModel = mainViewModel
You'll need to use below syntax for that :
android:entries="#{createExpenseConverter.getExpenseCategoryListFromSource(createExpenseViewModel.getAllSourceItems(1))}"
Here, what we've done is accessed your input from MainViewModel object createExpenseViewModel using getAllSourceItems() method;
And then passing it to another class createExpenseActivityConverter object createExpenseConverter using method getExpenseCategoryListFromSource() which returns you ArrayList<String> that your spinner requires.
Edit:
When you use LiveData in DataBinding, Data-binding Compiler takes care of refreshing data just like ObservableFields. All you need to do is provide your LifeCycleOwner to your databinding object.
For Example:
If your activity has ViewDataBinding let's say mActivityBinding using which you provide your ViewModel to set LiveData in xml binding, then after setting your ViewModel consider setting LifecycleOwner like below code :
//Some Activity having data-binding
... onCreate() method of activity
mActivityBinding.setViewModel(myViewModel);
mAcivityBinding.setLifecycleOwner(this); // Providing this line will help you observe LiveData changes from ViewModel in data-binding.
...
Refer here
Related
I watched this awesome talk by Florina Muntenescu on KontlinConf 2018 where she talked about how they reshaped their app architecture.
One part of the talk was how they expose a UiModel (not ViewModel) via LiveData from the ViewModel. (watch here)
She made a example similar to this:
class MyViewModel constructor(...) : ViewModel() {
private val _uiModel = MutableLiveData<UiModel>()
val uiModel: LiveData<UiModel>
get() = _uiModel
}
A view declaration for the ViewModel above could be:
<layout>
<data>
<variable
name="viewModel"
type="com.demo.ui.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="#+id/text"
android:text="#={viewModel.uiModel.text}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
She didn't talked about (or I missed it) how they react to property changes within the UiModel itself. How can I execute a function everytime text changes?
When having the text in separate LiveData property within the ViewModel I could use MediatorLiveData for this like:
myMediatorLiveData.addSource(text){
// do something when text changed
}
But when using the approach above the UiModel does not change instead the values of it are changed. So this here doesn't work:
myMediatorLiveData.addSource(uiModel){
// do something when text inside uiModel changed
}
So my question is how can I react on changes inside a UiModel in the ViewModel with this approach?
Thanks for advice,
Chris
I want to summarize my research regarding the topic above.
As #CommonsWars said in the comments above you can implement the field in the UiModel as ObservableFields. But after some hands on I currently prefer the approach described here.
This led me to the following code:
ViewModel:
class MyViewModel constructor(...) : ViewModel() {
val uiModel = liveData{
val UiModel uiModel = UiModel() // get the model from where ever you want
emit(uiModel)
}
fun doSomething(){
uiModel.value!!.username = "abc"
}
}
UiModel
class UiModel : BaseObservable() {
#get:Bindable // puts '#Bindable' on the getter
var text = ""
set(value) {
field = value
notifyPropertyChanged(BR.text) // trigger binding
}
val isValid: Boolean
#Bindable("text") get() { // declare 'text' as a dependency
return !text.isBlank()
}
}
Layout
<layout>
<data>
<variable
name="viewModel"
type="com.demo.ui.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="#+id/text"
android:text="#={viewModel.uiModel.text}" />
<Button
android:id="#+id/sent_btn"
android:enabled="#{viewModel.uiModel.isValid}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I've chosen this approach because of the way we change properties of the UiModel from within a ViewModel.
We can you can set/get the username in the ViewModel by:
fun doSomething(){
uiModel.value!!.username = "abc"
}
fun doSomething(){
uiModel.value!!.username
}
When you implementing it by ObservableFields you have to set/get the username by:
fun doSomething(){
uiModel.value!!.username.set("abc")
}
fun doSomething(){
uiModel.value!!.username.get()
}
You can pick the approach which suites best for your needs!
Hope this helps someone.
Chris
One Activity, two Fragments that share a common ViewModel. I have verified that the view model reference is the same in each fragment.
In the layout XML for fragment one, there is a TextInputLayout. Fragment two updates the view model with a boolean value. The text input layout is observing this value and should call a BindingAdapter when the value is changed.
The binding adapter fires when the fragments are instantiated and their layouts are inflated, so I know that the view is observing this value. However, later on, when fragment two updates the value, the view in fragment one does not trigger the binding adapter.
This is in onCreateView() of fragment one:
registrationViewModel = activity?.run {
ViewModelProviders
.of(this, RegistrationViewModelFactory(prefs, dataFetcherService))
.get(RegistrationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
and this is the view that is observing that view model:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/reg_auth_code_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
bind:errorState="#{registrationViewModel.registrationData.authorizationError}"
bind:errorMessage="#{#string/invalid_auth_code}">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/reg_auth_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{registrationViewModel.registrationData.authCode}"
android:hint="#string/enter_auth_code"
android:maxLines="1"
android:inputType="text"
android:imeOptions="actionDone"
app:autoSizeTextType="uniform"/>
</com.google.android.material.textfield.TextInputLayout>
As for fragment two, same code in onCreateView():
registrationViewModel = activity?.run {
ViewModelProviders
.of(this, RegistrationViewModelFactory(prefs, dataFetcherService))
.get(RegistrationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
When a button is clicked, fragment two fires an activity in the view model:
private fun attemptNavigationToUserData() {
viewModelScope.launch {
isAuthorized = runBlocking { useCase.isAuthorized() }
registrationData.value?.authorizationError = !isAuthorized
}
}
And finally, here is the BindingAdapter:
#BindingAdapter("errorState", "errorMessage")
fun setErrorState(
textInputLayout: TextInputLayout?,
errorState: Boolean,
errorMessage: String) {
textInputLayout?.let {
it.isErrorEnabled = errorState
if (errorState) it.error = errorMessage
}
}
This all seems to be set up correctly, AFAIK. As I mentioned, the binding adapter fires when the views are initially inflated, but never again.
Why isn't my XML observing the view model? Or, why isn't the binding adapter firing upon update??
Thanks for any help.
The answer is most likely that you don't set the lifecycleOwner for your fragment binding object.
For further information see https://stackoverflow.com/a/56011798/1894338
Look at my answer here https://stackoverflow.com/a/66488334/9747826
setLifeCyclerOwner and setting the viewModel are the key.
You authorizationError should be a LiveData<>.So that the BindingAdapter mothod will called automatically when the LiveData's value(errorState) is updated.
If you want BindingAdapter method receive the automatic update, you should use LiveData in the dataBinding expression.
Such as:
subTitleText="#{removableItemsViewModel.removableItemsInfo}"
and the BindingAdapter method and the LiveData:
val removableItemsInfo: LiveData<Pair<Int, Long>>
-----------------------------------------------------
#BindingAdapter("subTitleText")
fun setSubTitleText(textView: TextView, pair: Pair<Int, Long>){
}
also,don't forget to invoke binding.setLifecycleOwner()
Android data binding does not observe kotlin's liveData builder
the following code will create a LiveData and it's supposed to be observed by data binding in XML but it doesn't work
val text =
liveData(Dispatchers.Default) {
emit("Hello")
}
on the other hand if it's gets observed in Kotlin it works fine
vm.text.observe(lifeCycleOwner,{
binding.texti.text = it
})
the xml:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/texti"
android:text="#{viewModel.text}"
/>
and if i change the definition of live data to:
val text = MutableLiveData("Hello")
data binding automatically observes and sets the text
Am i doing it wrong or it's a bug?
Did you set lifecycle owner of the binding? The binding initialization should look like:
override fun onCreate(savedInstanceState: Bundle?) {
val binding = DataBindingUtil.setContentView<MyActivityBinding>(this, R.layout.my_activity)
binding.setLifecycleOwner(this)
binding.viewModel = viewModel.get()
...
}
Fore more details, see: Use LiveData to notify the UI about data changes
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