I'm learning RecyclerView with ListAdapter.
1: I find setControl() in init{ } isn't be launched after I run notifyDataSetChanged() , why?
2: What code should I place it in init{ }? What code should I place it in fun bind(aMVoice: MVoice{ } ?
Code
class VoiceAdapters (private val aHomeViewModel: HomeViewModel):
ListAdapter<MVoice, VoiceAdapters.VoiceViewHolder>(MVoiceDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VoiceViewHolder {
return VoiceViewHolder(
LayoutVoiceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: VoiceViewHolder, position: Int) {
val aMVoice = getItem(position)
holder.bind(aMVoice)
}
inner class VoiceViewHolder (private val binding: LayoutVoiceItemBinding):
RecyclerView.ViewHolder(binding.root) {
init {
setControl()
}
fun bind(aMVoice: MVoice) {
binding.amVoice = aMVoice
binding.executePendingBindings()
}
fun setControl(){
binding.aHomeViewModel = aHomeViewModel
binding.chSelect.setOnCheckedChangeListener{ _, isChecked ->
binding.amVoice?.let {
...
}
}
...
}
}
}
Added Content:
To ADM: Thank you very much!
A: Why isn't it a good idea to passing a ViewModel to adapter ?
B: How can I use interface instead ? Could you show me some sample code?
BTW, the following item layout of RecyclerView need to use ViewModel aHomeViewModel to control whether the CheckBox chSelect is shown or not. I will set the value of aHomeViewModel.displayCheckBox in a fragment.
layout_voice_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable name="aMVoice"
type="info.dodata.voicerecorder.model.MVoice" />
<variable name="aHomeViewModel"
type="info.dodata.voicerecorder.viewcontrol.HomeViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="#+id/chSelect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}"
android:text="" />
<TextView
android:id="#+id/voiceID"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{Integer.toString(aMVoice.id)}" />
</LinearLayout>
</layout>
RecyclerView reuse same ViewHolder multiple times that's why constructor does not get called . So any binding Stuff that should be done for all items should written inside onBindViewHolder.
On calling notifyDataSetChanged onBindViewHolder will get called for positions which are visible on screen and the same ViewHolder will be reused (Well it depends). But the thing here is a new ViewHolder will not be created each time so you can not us constructor for such operation.
What code should I place it in init{ }? What code should I place it in fun bind(aMVoice: MVoice{ } ?
Well inside init you can find views and set the action listeners. Inside bind you do the stuff for each item i.e setting data to the views.
On other hand you should not be passing a ViewModel to adapter that's not a good idea, Use an interface instead.
Why you don't pass ViewModel to Adapter
Well ultimately ViewModel is just a class so you can pass it and it won't give any error . The reasons i can think of right now to not do it are follows :-
By Passing ViewModel yo are tightly coupling the adapter to a single ViewModel i.e in turns Activity or a fragment. Now you can not reuse this Adapter any any other place
Also the whole point of having a ViewModel is to observe the data stream which don't usually happen inside Adapter.
Remember one thing ViewModel(LiveData) is not a replacement of Callback interface. So you should be using callback interface here Since you do not need a lifecycle component here.
So instead of passing ViewModel direactly pass the dataset to the adapter And if you need to notify the Activity or Fragment on the actions (click, long click) use a Interface.
init will be called once when the ViewHolder is created. Thats why RecyclerView has the word recycle in it. It will reuse its objects. Therefor init will only be called once in the Lifecycle of ViewHolder. bind will be called, when the content is visible (or near visible). Call setControl in bind.
Related
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 want to inject my viewModel inside RecyclerView with Hilt.
It can be inject but viewModel not destroy when recyclerView destroyed.
what is the best way to inject viewModel inside recyclerView with hilt?
The best way to do this is to create separate adapter and viewholder classes and then you can inject your viewModel inside that viewholder class instead of the adapter.
To destroy the viewModel you should manually do it by observing the parentlifecycle. when the parent lifecycle event is ON_DESTROY do something similar to this in the init block of the adapter class.
parentLifecycle.addObserver(object : LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onParentDestroy() {
recyclerView?.run {
for (i in 0 until childCount) {
getChildAt(i)?.let {
(getChildViewHolder(it) as BaseItemViewHolder<*, *>)
.run {
onDestroy()
viewModel.onManualCleared()
}
}
}
}
}
}
Here onManualCleared() function calls onCleared().
A view model shouldn't be injected inside an adapter, as I read in the comments you can you a better way than that,Let's imagine you have an adapter with many rows, each row when the user clicks on it, it performs a network call.
First, create an interface
interface Click {
fun onClick(index: Int, item: Model)
}
inside your adapter, init an instance of it then use it in your onBindViewHolder
yourview.setOnClickListener {v-> interface.onClick()}
don't forget to init the interface whether the place you're using it (Activity/Fragment/...).
This is a better solution than using a ViewModel for every row, which may lead to a SystemLeaks.
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()
I'm trying out the new Android Architecture components and have run into a road block when trying to use the MVVM model for a custom view.
Essentially I have created a custom view to encapsulate a common UI and it's respective logic to use throughout the app. I can set up the ViewModel in the custom view but then I'd have to either use observeForever() or manually set a LifecycleOwner in the custom view like below but neither seem correct.
Option 1) Using observeForever()
Activity
class MyActivity : AppCompatActivity() {
lateinit var myCustomView : CustomView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myCustomView = findViewById(R.id.custom_view)
myCustomView.onAttach()
}
override fun onStop() {
myCustomView.onDetach()
}
}
Custom View
class (context: Context, attrs: AttributeSet) : RelativeLayout(context,attrs){
private val viewModel = CustomViewModel()
fun onAttach() {
viewModel.state.observeForever{ myObserver }
}
fun onDetach() {
viewModel.state.removeObserver{ myObserver }
}
}
Option 2) Setting lifecycleOwner from Activity`
Activity
class MyActivity : AppCompatActivity() {
lateinit var myCustomView : CustomView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myCustomView = findViewById(R.id.custom_view)
myCustomView.setLifeCycleOwner(this)
}
}
Custom View
class (context: Context, attrs: AttributeSet) : RelativeLayout(context,attrs){
private val viewModel = CustomViewModel()
fun setLifecycleOwner(lifecycleOwner: LifecycleOwner) {
viewModel.state.observe(lifecycleOwner)
}
}
Am I just misusing the patterns and components? I feel like there should be a cleaner way to compose complex views from multiple sub-views without tying them to the Activity/Fragment
1 Option -
With good intention, you still have to do some manual work - like, calling onAttach\ onDetach Main purpose of Architecture components is to prevent doing this.
2 Option -
In my opinion is better, but I would say it's a bit wrong to bind your logic around ViewModel and View. I believe you can do same logic inside Activity/Fragment without passing ViewModel and LifecycleOwner to CustomView. Single method updateData is enough for this purpose.
So, in this particular case, I would say it's overuse of Architecture Components.
it doesn't make sense to manage the lifecycle of the the view manually by passing some reference of the activity to the views and calling onAttach/onDetach, when we already have the context provided when that view is created.
I have a fragment in a NavigationView that has other fragments in a view pager, more like a nested fragment hierarchy scenario.
I have some custom views in these top-level fragments, when the custom view is directly in the top fragment, I can get an observer like this
viewModel.itemLiveData.observe((context as ContextWrapper).baseContext as LifecycleOwner,
binding.item.text = "some text from view model"
}
when I have the custom view as a direct child of an activity I set it up directly as
viewModel.itemLiveData.observe(context as LifecycleOwner,
binding.item.text = "some text from view model"
}
in these activities, if I have a fragment and it has some custom view and I use the 2nd approach, I get a ClassCastException(), and I have to reuse these custom views in different places, both activities, and fragments (that's the idea of having a custom view)
so i wrote an extension function to set the LifeCycleOwner
fun Context.getLifecycleOwner(): LifecycleOwner {
return try {
this as LifecycleOwner
} catch (exception: ClassCastException) {
(this as ContextWrapper).baseContext as LifecycleOwner
}
}
now i simply set it everywhere as
viewModel.itemLiveData.observe(context.getLifecycleOwner(),
binding.item.text = "some text from view model"
}
Trying to use Android's Data Binding to adapter for a ViewPager (controls slidable Fragments).
FooPagerAdapter.kt:
class FooPagerAdapter(fm: Fragmentmanager, private val mFragments: List<BarFragment>) : FragmentStatePagerAdapter(fm) {
override fun getItem(position: int): Fragment {
return mFragments(position)
}
override fun getCount(): Int {
return mFragments.size
}
}
If done from the Activity, it would look like:
..
mFooViewPager.adapter = FooPagerAdapter(fragmentFamanager, fragmentsList)
..
Question:
Now how does one transfer adapter functionality to the binding file to update fragments ViewPager using Data Binding?
Edit:
As I understand it has to be something like this.
activity_foo.xml:
<android.support.v4.view.ViewPager
..
app:fragments"${viewModel.fragments}"/>
And then in a FooViewModel.kt:
fun getFragments(): LiveData<List<BarFragment>>? = mFragments
companion object {
#BindingAdapter("bind:fragments")
fun setAdapter(pager: ViewPager, adapter: BarPagerAdapter) {
pager.adapter = adapter
}
}
Edit2:
Decided to use a ViewModel directly (without binding) to set ViewPager's adapter.
activity_foo.xml:
<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.foo.bar.viewmodels.FooViewModel"/>
</data>
..
<android.support.v4.view.ViewPager
..
app:adapter="%{viewModel.adapter}"/>
FooViewModel.kt:
class FooViewModel(application: Application) : AndroidViewModel(application) {
..
fun setAdapter(pager: ViewPager, fragments:List<PeriodFragment>) {
pager.adapter = PeriodsPagerAdapter(mFragmentManager!!, periods)
}
Getting:
Error:...layout\activity_foo.xml:39 attribute 'com.foo.bar:adapter' not found
A #BindingAdapter should be static in Java, thus be annotated with #JvmStatic. Additionally you're supposed to skip all namespaces with the binding attributes in the adapter definition. Also the second parameter needs to reference a type you want to set. In your case this is LiveData<List<BarFragment>>. Then you can create the adapter statically.
companion object {
#JvmStatic
#BindingAdapter("fragments")
fun setAdapter(pager: ViewPager, fragments: LiveData<List<BarFragment>>) {
pager.adapter = createAdapterForFragments(fragments)
}
}
But if fragments would be a PagerAdapter, a binding adapter is not necessary at all. As a default implementation the compiler looks for a given setter method for the attribute. So if you use app:adapter, the setAdapter() method will be used automatically. Therefore it should be sufficient to just put this adapter definition in the layout.
<android.support.v4.view.ViewPager
...
app:adapter"#{viewModel.fragments}"/>
I'd suggest to use the latter and setup the adapter with the viewModel not with the data binding.
A little data binding convenience for ViewPager and TabLayout you'll find with ViewPagerDataBinding.