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
Related
I'm trying to update TextInputEditText text via data-binding after I get some data from BE API call. My solution works perfectly if code is not executed inside coroutine. If variable is set inside coroutine EditText does not get updated.
My XML code:
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="40dp"
android:text="#={ viewModel.name }" />
My viewModel code:
var name: String = ""
get() = field.trim()
set(value) {
field = value
//some other unrelated code
}
...
fun getName(){
name = "first"
viewModelScope.launch(Dispatchers.Main) {
name = "second"
}
}
TextInputEditText will be updated to "first" but not to "second". I've tried with other dispatchers. I've also verified via debugger that "name" variable setter is being triggered both times. It's just not updating the EditText. Any ideas on what could cause this?
In my case, the problem was solved by setting the value of the lifecycleOwner property in the following code. The data binding is now done as intended.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postDetailViewModel = ViewModelProvider(this)[PostDetailViewModel::class.java]
binding.varPostDetailViewModel = postDetailViewModel
binding.lifecycleOwner = this // Add this line
coroutineScope.launch {
arguments?.let {
val args = PostDetailFragmentArgs.fromBundle(it)
postDetailViewModel.getPostDetail(args.postID)
}
}
}
Your name field needs to be observable.
Right now, nothing is telling the EditText that the field was updated and needs to be rebound. You're probably seeing "first" from initially setting the viewModel on the binding.
Review the documentation on obervability.
My answer to another similar question might also be helpful.
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()
I'm working with this network graph library.
I have a GraphSurfaceView in my layout.
This view displays graph by giving a NetworkGraph object to it, something like this:
graphSurface.init(networkGraph)
How can I bind networkGraph data to graphSurface in MVVM architecture?
I tried this code below and it didn't work, I should call init after creating my networkGraph:
BindingUtils.kt
#BindingAdapter("networkGraph")
#JvmStatic
fun setNetworkGraph(view: GraphSurfaceView, networkGraph: NetworkGraph) {
view.init(networkGraph)
}
myActivity.kt
relationActivityBinding = DataBindingUtil.setContentView(this, getLayoutId())
graphSurface = relationActivityBinding.graphSurface
viewModel.kt
fun getNetworkGraph() = this.graphManager?.getGraphNetwork() // returns my networkGraph
activity.xml
<giwi.org.networkgraph.GraphSurfaceView
android:id="#+id/graphSurface"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:networkGraph="#{viewModel.getNetworkGraph()}"/>
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
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}"