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()
Related
I'm new to MVVM. I'm trying to figure out easiest way to change view from ViewModel. In fragment part I have navigation to next fragment
fun nextFragment(){
findNavController().navigate(R.id.action_memory_to_memoryEnd)
}
But I cannot call it from ViewModel. AFAIK it is not even possible and it destroys the conception of ViewModel.
I wanted to call fun nextFragment() when this condition in ViewModel is True
if (listOfCheckedButtonsId.size >= 18){
Memory.endGame()
}
Is there any simple way to change Views depending on values in ViewModel?
Thanks to Gorky's respond I figured out how to do that.
In Fragment I created observer
sharedViewModel.changeView.observe(viewLifecycleOwner, Observer<Boolean> { hasFinished ->
if (hasFinished) nextFragment()
})
I created changeView variable in ViewModel. When
var changeView = MutableLiveData<Boolean>()
change to true, observer call function.
source:
https://developer.android.com/codelabs/kotlin-android-training-live-data#6
I have a button. When the button is clicked, the button and a textView are animated. The question is: how to get multiple views on the binding adapter? Is the way I did it correct?
<variable
name="variableTextViewDescription"
type="androidx.appcompat.widget.AppCompatTextView" />
fun bind(task: Task, viewModel: ToDoListViewModel) {
binding.task = task
binding.viewModel = viewModel
binding.variableTextViewDescription = binding.textViewDescription
binding.executePendingBindings()
}
#BindingAdapter(value = ["task", "textViewDescription"], requireAll = true)
fun ImageButton.setOnClickButtonMore(task: Task, textViewDescription: AppCompatTextView) {
if (task.isExpanded) {
toggleArrow(this, false, textViewDescription)
} else {
toggleArrow(this, true, textViewDescription)
}
this.setOnClickListener {
task.isExpanded = toggleArrow(it, task.isExpanded, textViewDescription)
}
}
<ImageButton
android:id="#+id/buttonMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:task="#{task}"
app:textViewDescription="#{variableTextViewDescription}"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="#drawable/ic_baseline_keyboard_arrow_down_24"
tools:ignore="ContentDescription" />
I can propose a solution for you, that maybe different from adding multiple Views to the same Binding Adapter.
You can add a MutableLiveData when changed by Button click, it starts the animation.
So, we will have a single MutableLiveData added to 2 Binding Adapters (the button binding adapter and the ImageView binding adapter).
when the value of the MutableLiveData changed, both binding adapters will fire and in both adapters load your animation.
I have 5 fragments and one parent fragment which takes care of the fragment navigation. All these fragments use a common (shared) view model to share data across all fragments. But when I try to call a method from XML on button's onClick, it doesn't get called. I added breakpoint but the debugger didn't stop there at all.
Moreover, I have some visibility conditions defined in the view which are also using viewmodel reference and those don't execute either.
Below is the code of button click
<androidx.appcompat.widget.AppCompatButton
android:id="#+id/previousBTN"
style="#style/buttonStyleWithFillColor"
android:layout_width="wrap_content"
android:layout_height="#dimen/common_44"
android:layout_marginEnd="#dimen/common_50"
android:layout_marginBottom="#dimen/common_15"
android:clickable="#{!viewModel.isHorizontalProgress()}"
android:fontFamily="#font/lato_bold_style"
android:gravity="center"
android:onClick="#{()->viewModel.previous()}"
android:paddingStart="#dimen/common_20"
android:paddingEnd="#dimen/common_20"
android:text="#string/submit"
android:visibility="#{viewModel.currentTabIndex==5? View.VISIBLE:View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
In the above code, you can see a method call on button click and a visibility condition . None of them works.
CurrentTabIndex is a mutable live data defined in viewModel
var currentTabIndex = MutableLiveData(1)
This is the viewModel class
class EmpGPViewModel #ViewModelInject constructor(application: Application): BaseViewModel<EmpGPNavigator>(application) {
var model=MutableLiveData<EmpGPModel>()
var currentTabIndex = MutableLiveData(1)
fun selectDate() {
navigator.selectDate()
}
fun selectOutTime() {
navigator.selectOutTime()
}
fun next() {
navigator.next()
}
fun previous() {
navigator.previous()
}
fun submit() {
navigator.submit()
}}
I am also using Hilt in this project for dependency injection. I also have the same code setup on a different screen only difference is that it doesn't use a shared view model and there all of this code works.
I have a base fragment class which provides an overridden method to instantiate viewModel.
This is how I do it.
override val viewModel: EmpGPViewModel by viewModels()
Base fragment class has calls below code
viewDataBinding?.lifecycleOwner = viewLifecycleOwner
viewDataBinding?.setVariable(bindingVariable, mViewModel)
viewDataBinding?.executePendingBindings()
Inside the fragment of a tabbed activity:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
serverSetVM = ViewModelProvider(activity!!).get(ServersViewModel::class.java)
serverList = ArrayList(serverSetVM.get())
rv = rv_serverList // findViewById
rv.layoutManager = LinearLayoutManager(context)
rv.adapter = ServerListRevAdapter(context!! ,serverList)
serverSetVM.serverSetLiveData.observe(viewLifecycleOwner, Observer {
Log.v ("MainAct", "Inside OBSERVER")
serverList = ArrayList(serverSetVM.get())
rv.adapter!!.notifyDataSetChanged()
})
}
Also;
val serverSetLiveData = MutableLiveData<HashSet<Server>>() // Inside ViewModel class
observe() function does not seem to work. When the value of ServerSetVM is modified inside the same fragment (by the functions defined in ViewModel class, i.e. add()), recyclerView is not updated. According to Logcat output, Observer lambda is called only after onCreateView().
I confirmed MutableLiveData gets updated but Observer{} lambda is not called. Do I need to correct my notion about ViewModels?
EDIT (SOLUTION):
Use " = " operator to modify the MutableLiveData value so that observer can detect it. Even serverSetLiveData.value=serverSetLiveData.value does the job.
Observer only observe if you call setValue() or postValue() method of MutableLiveData
where you are calling the setValue() or postValue() function for serverSetLiveData .
eg. serverSetLiveData.setValue(serverList) or serverSetLiveData.postValue(serverList). in the code.
Obervre gets triggered only if you call .value =
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