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
Related
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
}
I have an app with one activity and two fragments, in the first fragment, I should be able to insert data to the database, in the second I should be able to see the added items in a recyclerView.
So I've made the Database, my RecyclerView Adapter, and the ViewModel,
the issue is now how should I manage all that?
Should I initialize the ViewModel in the activity and call it in some way from the fragment to use the insert?
Should I initialize the viewmodel twice in both fragments?
My code looks like this:
Let's assume i initialize the viewholder in my Activity:
class MainActivity : AppCompatActivity() {
private val articoliViewModel: ArticoliViewModel by viewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
}
Then my FirstFragments method where i should add the data to database using the viewModel looks like this:
class FirstFragment : Fragment() {
private val articoliViewModel: ArticoliViewModel by activityViewModels()
private fun addArticolo(barcode: String, qta: Int) { // function which add should add items on click
// here i should be able to do something like this
articoliViewModel.insert(Articolo(barcode, qta))
}
}
And my SecondFragment
class SecondFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private val articoliViewModel: ArticoliViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recyclerView)
val adapter = ArticoliListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
// HERE I SHOULD BE ABLE DO THIS
articoliViewModel.allWords.observe(viewLifecycleOwner) { articolo->
articolo.let { adapter.submitList(it) }
}
}
}
EDIT:
My ViewModel looks like this:
class ArticoliViewModel(private val repository: ArticoliRepository): ViewModel() {
val articoli: LiveData<List<Articolo>> = repository.articoli.asLiveData()
fun insert(articolo: Articolo) = viewModelScope.launch {
repository.insert(articolo)
}
}
class ArticoliViewModelFactory(private val repository: ArticoliRepository): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ArticoliViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return ArticoliViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Whether multiple fragments should share a ViewModel depends on whether they are showing the same data. If they show the same data, I think it usually makes sense to share a ViewModel so the data doesn't have to be pulled from the repository when you switch between them, so the transition is faster. If either of them also has significant amount of unique data, you might consider breaking that out into a separate ViewModel so it doesn't take up memory when it doesn't need to.
Assuming you are using a shared ViewModel, you can do it one of at least two different ways, depending on what code style you prefer. There's kind of a minor trade-off between encapsulation and code duplication, although it's not really encapsulated anyway since they are looking at the same instance. So personally, I prefer the second way of doing it.
Each ViewModel directly creates the ViewModel. If you use by activityViewModels(), then the ViewModel will be scoped to the Activity, so they will both receive the same instance. But since your ViewModel requires a custom factory, you have to specify it in both Fragments, so there is a little bit of code duplication:
// In each Fragment:
private val articoliViewModel: ArticoliViewModel by activityViewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
Specify the ViewModel once in the MainActivity and access it in the Fragments by casting the activity.
// In Activity: The same view model code you already showed in your Activity, but not private
// In Fragments:
private val articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel
Or to avoid code duplication, you can create an extension property for your Fragments so they don't have to have this code duplication:
val Fragment.articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel
I am very new using Android architecture components, so I decided to base my application using GithubBrowserSample to achieve many of my use cases. But i have the problem that i don´t know what is the correct way to share viewmodels between fragments with this approach.
I want to share the view model because i have a fragment with a viewpager with 2 fragments that need to observe data of the parent fragment view model
I used this before to achieve it, based on google's documentation
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
} ?: throw Exception("Invalid Activity")
}
but with the lifecycle-extensions:2.2.0-alpha03 seems to be deprecated
In GithubBrowserSample they have something like this to create an instance of a view model, but with this it seems to be a different instance every time
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val userViewModel: UserViewModel by viewModels {
viewModelFactory
}
And i don't know where to pass the activity scope or if I should pass it.
I tried something like this
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myViewModel = activity?.run {
ViewModelProvider(this, viewModelFactory).get(MyViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
but im getting
kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized
I hope you can help me, i'm a little lost with this, sorry if my english it´s not that good
by viewModels() provides a ViewModel that is scoped to the individual Fragment. There's a separate by activityViewModels() that scopes the ViewModel to your Activity.
However, the direct replacement for ViewModelProviders.of(this) is simply ViewModelProvider(this) - you aren't required to switch to by viewModels() or by activityViewModels() when using lifecycle-extensions:2.2.0-alpha03
In my app I have some data that will be used across all app in the different fragments. According to the Official Android Guides we should use LiveData and SharedViewModel
That documentations shows just how to use data from SharedViewModel in fragment. But ...
How to use that data in the FragmentViewModel?
Use case #1: using the SharedInfo from SharedViewModel I need to make some request to the server and to do smth with response from server in the FragmentViewModel
Use case #2: I have some screen (fragment) that shows info both from FragmentVM and SharedVM
Use case #3: When user click on SomeButton I need to pass some data from SharedViewModel to the ViewModel
I have found two possibles ways how to do it (maybe their are very similar), but I seems that it can be done more clearly
1) Subscribe to LiveData from SharedViewModel in the fragment and call some method in the ViewModel
2) Use the "CombineLatest" approach like in the RX ( thanks for https://github.com/adibfara/Lives )
Some example to reproduce:
class SharedViewModel(app: Application) : ViewModel(app) {
val sharedInfo = MutableLiveData<InfoModel>()
}
class MyFragmentViewModel(app: Application) : ViewModel(app) {
val otherInfo = MutableLiveData<OtherModel>()
}
class StartFragment : Fragment(){
lateinit var viewModel: MyFragmentViewModel
lateinit var sharedViewModel: SharedViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Create Shared ViewModel in the Activity Scope
activity?.let {
sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)
}
// Create simple ViewModel forFragment
viewModel = ViewModelProviders.of(this).get(MyFragmentViewModel::class.java)
// Way #1
sharedViewModel.sharedInfo.observe(this, Observer{
viewModel.toDoSmth(it)
})
viewModel.otherInfo.observe(this, Observer{
sharedViewModel.toDoSmth(it)
})
// Way #2
combineLatest(sharedViewModel.sharedInfo, viewModel.otherInfo){s,o -> Pair(s,o)}.observe(this, Observe{
viewModel.doSmth(it)
// or for example
sharedViewModel.refreshInfo(it)
})
}
}
I expect to found some clear way to access to LiveData from SharedVM from FragmentVm and vise versa. Or maybe I think wrong and this is a bad approach to do that and I shouldn't do it
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()