I'm facing some issues with a MediatorLiveData inside a fragment.
For example:
I have a View Model:
class InfoPessoalViewModel : NavigationViewModel(){
//fields
val nameField = MutableLiveData<String>()
val formMediator = MediatorLiveData<Boolean>()
init {
formMediator.addSource(nameField){}
}
And I'm putting this name inside my xml by databinding
<EditText
android:id="#+id/name"
android:text="#{viewModel.nameField}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName" />
But the observer its not firing inside my fragment.
bindingView.apply {
lifecycleOwner = this#InfoFragment
viewModel = viewModel
}
viewModel.formMediator.observe(this, Observer {
Log.d("Mediator","Fired!")
})
Anyone knows what I'm doing wrong here?
EDIT
I have changed to two-way binding here
android:text="#={viewModel.nameField}"
But none of this have fired yet
viewModel.nameField.observe(this, Observer {
Log.d("Livedata","Fired!")
})
viewModel.formMediator.observe(this, Observer {
Log.d("Livedata","Fired!")
})
EDIT 2
I'm importing this viewModel, like this:
<data>
<variable
name="viewModel"
type="br.com.original.bank.sejaoriginal.steps.infopersonal.InfoPessoalViewModel" />
</data>
And binding view inside my fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
bindingView = DataBindingUtil.inflate(inflater,R.layout.fragment_info_pessoal,container,false)
return bindingView.root
}
EDIT 3
So, the initial problem was with viewModel = viewModel, with wrong reference inside apply method.
But the problem with MediatorLiveData not being called still
Check those steps in sequence:
1) Change this:
android:text="#{viewModel.nameField}"
to this (note the additional equals symbol) :
android:text="#={viewModel.nameField}"
More info about 2-way data binding here
2) Check that you added the correct viewmodel binding in the XML layout:
3) Check the code binding, change your binding code to this:
bindingView.apply {
lifecycleOwner = this#InfoFragment
viewModel = this#InfoFragment.viewModel
}
One thing that helped me, although this post did not have the problem I had:
if you have a function that returns a livedata, like fun myName(): LiveData {
return myLiveName
},
the view model binding will not show value in xml layout. The live data has to be a variable, not funtion, like:
val myNameVariable: LiveData = myName()
Related
What I would like to do is to remove at each fragment such lines:
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#SomeFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
all these lines can be placed in onCreateView or some other lifecycle methods. I have a lot of fragment, so as a result a lot of such code scopes. I think it is not logical to have similar code inside every file. At first I thought about DI, but as I got to know (maybe I'm wrong) that it is not possible to use DI with ViewModel where I have some constructor parameters. So, I started thinking about creating some single CoreFragment or something like that for having such scope and then make implementation of this CoreFragment inside all other fragment. Let see some example: we have such SampleFragment with lines which I would like to optimize:
class SomeSampleFrg : Fragment(R.layout.fragment_sample) {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding = FragmentSampleBinding.bind(view)
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#SampleFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
....
}
...
}
then I created CoreFragment:
open class CoreFragment: Fragment() {
lateinit var viewModel: AppViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#CoreFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
}
}
and then the field viewModel will be accessible after implementation of this fragment class. Is it possible to do? Or it is very stupid question?)) The only problem which I can not even imagine how to do in addition is how to work with DataBinding in such situation) I mean that I will need to make some more fields and also pass layout to this CoreFragment. Maybe someone has any ideas how to do it?
I am trying to learn how to use MVVM, and two way data binding in Android. I am quite familiar with MVVM and two way data binding from other languages/frameworks (.net, Angular etc)
So, from what I can see, I want a viewModel to retain data, and I also want a repository that I will be passing to a service (eg to play an audio file)
I set up the View model following this tutorial, but i does cover the UI data binding. I'ev looked at a LOT of posts and doco, and there seems to be a lot of different ways of using ViewModel, so is a bit confusing.
In my main activity, I have the following...
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initialise()
}
private fun initialise() {
try {
val factory = InjectorUtils.provideViewModelFactory()
val viewModel = ViewModelProvider(this, factory).get(MainActivityViewModel::class.java)
viewModel.mode.observe(this, Observer<Int> { mode ->
// Update
});
} catch (ex: Exception) {
val m = ex.message;
}
}
and the ViewModel contains
package com.example.myApp
import androidx.databinding.Bindable
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.myApp.Modes
// View model to hold all UI state
class MainActivityViewModel(private val repository: Repository): ViewModel() {
val mode: MutableLiveData<Int> = MutableLiveData<Int>(0)
val stopColour: MutableLiveData<String> = MutableLiveData<String>("hello")
init{
mode.value = Modes.AUTO_MIDI
stopColour.value = "123"
}
}
The layout has the variable declared...
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.example.myapp.MainActivityViewModel" />
</data>
<GridLayout
...
<RadioButton
android:textColor="#FCFFFEFE"
android:id="#+id/radioButtonAuto"
android:layout_gravity="fill_horizontal"
android:layout_row="1"
android:layout_column="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={viewModel.stopColour}"> <--- my test binding
</RadioButton>
When I put a break point in the main activity, I can see the ViewModel is created, and has values in it...
There are no exceptions, however, the bound text ("123") just does not show up in the UI.
What could I be missing here?
You need to use DataBindingUtils to bind the view to activity instead of using setContentView.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.lifecycleOwner = this
binding.viewModel = viewModel [Your viewModel class object]
}
basically you need to define the viewLifecycle owner for binding and a value for the variable you created in xml file.
Two way databinding is helpful in cases like EditText. In case of Radio Button or TextView etc you can simply use binding.
android:text="#{viewModel.stopColour}"
The above code snippet will work fine in case of radio Button.
For more details you can visit android developer documentation link below.
https://developer.android.com/topic/libraries/data-binding
you need to init your view binding viewModel to Your activity viewModel object
val activityViewModel = ViewModelProvider(this,factory).get(MainActivityViewModel::class.java)
your view binding.viewModel = activityViewModel
I am learning MVVM and I am confused with binding activity and its view.
in Data Binding course, it says, I need to
binding = DataBindingUtil.bind(view) // binding is in the field.
But MVVM course, it says, I need to
MainActivityBinding.inflate(layoutInflater).also{
binding = it // binding is in the field
setContentView(it.root)
}
I am using the second one and daat binding expression in XML doesn't work.
I don't know if it's a good access but, I am trying to get the data from ViewModel class.
So, What I did is,
<layout>
<data>
<variable
name="viewModel"
type="com.example.my_app.ui.main.MainViewModel"/>
</data>
<TextView
android:text="#{viewModel.user.name}"
/>
<TextView
android:text="#{viewModel.getUserHeight()}
/>
</layout>
The ViewModel is
class MainViewModel(
a: UserData,
): ViewModel() {
private val _userData = MutableLiveData<UserData>()
val userData: LiveData<UserData>
get() = _userData
init {
_userData.value = a
Log.i("view-model", "${_userData.value?.name}")
}
fun getUserHeight():String{
return "${a.value.height}cm"
}
override fun onCleared() {
super.onCleared()
Log.i("view-model", "MainViewModel destroyed")
}
}
It doesn't work. Could you please explain which part is incorrect?
In your activity, you should do something like this :
val mainViewModel = MainViewModel()
// I dont know how you provide viewModel in your project, it doesn't matter in this case.
fun onCreate(bundle : Bundle){
val binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = mainViewModel
}
It's hard to understand what the problem is from the headline - I'll try my best explaining:
I'm using Koin for dependency injection. I'm injecting my HomeViewModelinto my HomeFragment (the viewModel has parameters, but that should be unrelated to the problem):
// fragment code
private var viewModelParameters: ParametersDefinition? = null
lateinit var viewModel: VM
...
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layout, container, false)
binding?.lifecycleOwner = viewLifecycleOwner
viewModel = getViewModel(HomeViewModel::class, parameters = viewModelParameters)
return binding?.root ?: inflater.inflate(layout, container, false)
}
The fragment contains a RecyclerView. The recycler's ViewHolder declares an interface, that is injected via Koins by inject()`:
class MyRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), KoinComponent {
private val callback by inject<Callback>()
fun bind(item: MyItemType) {
itemView.setOnClickListener { callback.myCallbackFunction(item) }
}
interface Callback {
fun myCallbackFunction(item: MyItemType)
}
}
My HomeviewModel implements this interface, and I bind it to the viewModel in my KoinGraph module via Koin's bind DSL method:
private val baseModule = module {
single { androidApplication().resources }
single { PermissionHelper(androidApplication()) }
...
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
}
Now, when I click on my recycler item, the callback's myCallBackFunction is called, which should trigger the implementation in my HomeViewModel. Which it does, but: It is not the same instance, but a new HomeViewmodel.
My understanding is that Android's ViewModelclass, if used in the typical way (currently using, without Koin, by viewModels() - see here), should only exist once. But with Koin's viewModel{} call, I can create multiple instances, which I think I shouldn't be able to? Or should I (and if yes, why)?
Anyway, I'd like to bind my callback to the view model I already have (the one the fragment knows of) and not a new instance my fragment doesn't know about.
How can I achieve that using Koin and its injection patterns?
By the way, If I use
single { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
instead of
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
my code works as intended - since I'm forcing my view model to be a singleton that way - which is what I want. But what is the point of the viewModel{} command then? And are there any downsides to it? It doesn't feel like what I should be supposed to do but maybe it's totally fine?
What is the proper way to keep the actual app state in MVVM? A simple example to describe what I mean: I have two fragments and one global variable or object of my Class. I can change this variable or object on both fragments. Where should I keep this in code?
The most easiest way is to use KTX extension function activityViewModels<VM : ViewModel>
see here.
From the doc:
Returns a property delegate to access parent activity's ViewModel ...
It will retrieve the ViewModel instance provided by the ViewModelProviders of the activity the fragments are attached to.
So any change on the view model instance will be reflected on all fragments.
Here a simple example:
class MVModel: ViewModel() {
var count = MutableLiveData(0)
fun increment() {
count.value = count.value!!.plus(1)
}
}
class MFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentMBinding.inflate(inflater, container, false)
val viewModel by activityViewModels<MVModel>()
binding.lifecycleOwner = this // <-- this enables MutableLiveData update the UI
binding.vm = viewModel
return binding.root
}
}
You can make shared view model where all your components will access easily