UI not updated when ViewModel value changed - android

I have created sample app to demo my question:
A TextView and a Button, text view visibility is bound to viewModel.bar
I want the button to switch the value of viewModel.bar when clicked and the UI to get updated as well.
However, this is not happening. The value is changed but the UI is not updated.
Layout File:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable name="viewModel"
type="com.example.bindingone.MainViewModel"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World"
android:visibility="#{viewModel.bar ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:text="Update UI"
android:onClick="clicked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</android.support.constraint.ConstraintLayout>
</layout>
MainActivity File:
class MainActivity : AppCompatActivity() {
private val viewModel = MainViewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = viewModel
}
fun clicked(v: View) {
viewModel.bar.value = viewModel.bar.value?.not() ?: false
}
}
MainViewModel file:
class MainViewModel : ViewModel() {
var bar = MutableLiveData<Boolean>()
}

After creating binding, add this line binding.setLifecycleOwner(this).
/**
* Sets the {#link LifecycleOwner} that should be used for observing changes of
* LiveData in this binding. If a {#link LiveData} is in one of the binding expressions
* and no LifecycleOwner is set, the LiveData will not be observed and updates to it
* will not be propagated to the UI.
*
* #param lifecycleOwner The LifecycleOwner that should be used for observing changes of
* LiveData in this binding.
*/
#MainThread
public void setLifecycleOwner(#Nullable LifecycleOwner lifecycleOwner)

Related

On Click event Does not working with Data Binding in Android Studio

I am learning DataBinding in android studio. But I am facing a problem with binding a ModelView. I want to bind a function with a button on click event. I set a function in the model view. I want to update my text view with on click event of my button. But When I click the button my text is not updating. I can not understand what I have done wrong.
XML layout:
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="model"
type="com.example.jetpack.MainModelView" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{model.title}"
android:textSize="28sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Update Text"
android:onClick="#{()-> model.update()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tv" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Main Model View:
class MainModelView : ViewModel() {
var title: String = " This is My Application"
fun update() {
title = "I am Changed"
Log.d("UPDATE", "update successfully from main model view")
}
}
Main Activity:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var mainModelView: MainModelView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
mainModelView = ViewModelProvider(this).get(MainModelView::class.java)
binding.model = mainModelView
}
}
My app Image:
thanks in advance for helping.
The title variable in ViewModel needs to be a ObservableField or LiveData otherwise you xml will never know when it's value got updated –
class MainViewModel : ViewModel() {
var text = MutableLiveData(" Welcome to my application ")
fun updateText() {
text.value = " Text is updated successfully "
}
}
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// Creating MainViewModel object
mainViewModel = MainViewModel()
// Binding mainViewModel variable
// with MainViewModel object
binding.mainViewModel = mainViewModel
// passing LifeCycleOwner to binding object
binding.lifecycleOwner = this
}
}

Click Handling on Views in Clean Architecture

I have to implement click listener using binding and ViewModel as per clean architecture.
I have two button to select language like English and Chinese.
LanguageActivity.kt
#AndroidEntryPoint
class LanguageActivity : PBActivity(R.layout.activity_language) {
private val mViewModel:LanguageViewModel by viewModels()
private val mBinding:ActivityLanguageBinding by viewbind()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding.apply {
lifecycleOwner = this#LanguageActivity
viewModel = mViewModel
}
collectFlow(mViewModel.uiState){
Toast.makeText(this, it.name, Toast.LENGTH_SHORT).show()
when(it){
LanguageSelected.ENGLISH -> {
}
LanguageSelected.CHINESE -> {
}
LanguageSelected.NONE -> {
}
}
}
}
}
enum class LanguageSelected{
ENGLISH,CHINESE,NONE
}
LanguageViewModel.kt
#HiltViewModel
class LanguageViewModel #Inject constructor(
private val pbPrefs: PBPrefs
): ViewModel(){
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LanguageSelected.NONE)
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LanguageSelected> = _uiState
fun englishSelected()= viewModelScope.launch {
_uiState.value = LanguageSelected.ENGLISH
pbPrefs.setLanguageSelect()
}
fun urduSelected() = viewModelScope.launch {
_uiState.value = LanguageSelected.URDU
pbPrefs.setLanguageSelect()
}
}
activity_language.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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="uk.co.planetbeyond.telenorbluecollar.ui.language.LanguageViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.language.LanguageActivity">
<androidx.constraintlayout.widget.Guideline
android:id="#+id/center_vertical_gl"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<Button
android:id="#+id/englishTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="English "
android:onClick="#{() -> viewModel.englishSelected()}"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="#id/center_vertical_gl"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/urduTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Urdu"
android:onClick="#{() -> viewModel.urduSelected()}"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="#id/center_vertical_gl"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I have to select language on click of a button. What's the best approach as per clean architecture to implement
I have created an interface LanguageSelected but for that I have to created multiple methods in viewModel, As languages increase I have to create more methods in viewmodel.
How could I make it short or I mean extensible?

How to pass multiple views as parameters in BindingAdapter in Data binding android?

I have one relative layout and one ImageView. I want to set visibility based on Image loading like if image loads successfully then imageview is visible and if some error occurs relative layout is visible. How can I manage this scenario in data binding using BindingAdapter ?
Your question is not clear so I don't know if it will help you.
These are steps to implement
1: Create parameters in BindingAdapter.kt
#BindingAdapter("showLoading")
fun View.showLoading(loading: Boolean) {
if (loading) {
visible()
} else {
gone()
}
}
#BindingAdapter("showError")
fun View.showError(error: Boolean) {
if (error) {
visible()
} else {
gone()
}
}
fun View.gone() {
this.visibility = View.GONE
}
fun View.visible() {
this.visibility = View.VISIBLE
}
2: Use parameters [app:showLoading="#{viewModel.showLoading}"] and [app:showError="#{viewModel.showError}"] created in file 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>
<variable
name="viewModel"
type="com.xxx.xxx.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#EAEAE2"
android:orientation="vertical">
<Button
android:id="#+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ShowLoading" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:showLoading="#{viewModel.showLoading}" />
<RelativeLayout
android:id="#+id/viewError"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#color/colorPrimary"
app:showError="#{viewModel.showError}" />
</LinearLayout>
</layout>
3: Create showError and showLoading variables in Viewmodel.
And assign the viewModel variable to the binding.
class MainViewModel: ViewModel() {
val showError: MutableLiveData<Boolean> = MutableLiveData()
val showLoading: MutableLiveData<Boolean> = MutableLiveData()
init {
showError.postValue(false)
showLoading.postValue(true)
}
}
class MainActivity : AppCompatActivity() {
private val viewModel = MainViewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.lifecycleOwner = this
binding.viewModel = viewModel
initViews()
}
private fun initViews() {
binding.btnAdd.setOnClickListener {
viewModel.showError.postValue(true)
viewModel.showLoading.postValue(false)
}
}
}

Why does LiveData only effect main layout and doesn't LiveData effect child layout UI in Android Studio?

FragmentHome load layout_home.xml, and layout_home.xml displays a recyclerview and a button named btnMain
recyclerview include the item layout layout_voice_item.xml, it displays a button named btnChild。
I use displayCheckBox : LiveData<Boolean> to control whether both btnMain and btnChild are shown or not with the code android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}".
I find btnMain can be shown or not when I change the value of displayCheckBox, but btnChild keep to show, why?
FragmentHome.kt
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_home, container, false
)
binding.lifecycleOwner = this.viewLifecycleOwner
binding.aHomeViewModel=mHomeViewModel
val adapter = VoiceAdapters(mHomeViewModel)
binding.mvoiceList.adapter=adapter
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner){
adapter.submitList(it)
}
...
return binding.root
}
}
HomeViewModel.kt
class HomeViewModel(private val mDBVoiceRepository: DBVoiceRepository) : ViewModel() {
private val _displayCheckBox = MutableLiveData<Boolean>(true)
val displayCheckBox : LiveData<Boolean> = _displayCheckBox
fun setCheckBox(isDisplay:Boolean){
_displayCheckBox.value = isDisplay
}
...
}
VoiceAdapters.kt
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(aHomeViewModel, aMVoice)
}
inner class VoiceViewHolder (private val binding: LayoutVoiceItemBinding):
RecyclerView.ViewHolder(binding.root) {
fun bind(mHomeViewModel: HomeViewModel, aMVoice: MVoice) {
binding.aHomeViewModel = mHomeViewModel
binding.executePendingBindings()
}
}
...
}
class MVoiceDiffCallback : DiffUtil.ItemCallback<MVoice>() {
...
}
layout_home.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable name="aHomeViewModel"
type="info.dodata.voicerecorder.viewcontrol.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/mvoice_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/layout_voice_item"
/>
<Button
android:id="#+id/btnMain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
layout_voice_item.xml
<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="aHomeViewModel"
type="info.dodata.voicerecorder.viewcontrol.HomeViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="#+id/btnChild"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}"
/>
</LinearLayout>
</layout>
Giving a LifecycleOwner to LayoutVoiceItemBinding might helps.
voiceViewHolder.binding.lifecycleOwner = yourLifecycleOwner
The view holder pattern was not always part of android and before its wide adoption, a naive list implementation would have resulted in an inflated view rendered for each value in the submitted list. That is extremely memory inefficient.
With the adoption of a view holder pattern, views (the visual items) are only inflated up to the maximum that can be visible on the screen (and one or two extra for smooth scrolling)
The adapter however needs to be made aware of changes in the list's data.
https://developer.android.com/guide/topics/ui/layout/recyclerview#Adapter
If the list needs an update, call a notification method on the
RecyclerView.Adapter object, such as notifyItemChanged(). The layout
manager then rebinds any affected view holders, allowing their data to
be updated.
Observing the displayCheckBox : LiveData<Boolean> and calling adapter.notifyItemChanged might be the issue at hand.
I leave with another reference reiterating that view binding will not observe the live data but reduces the boilerplate code.
https://medium.com/androiddevelopers/android-data-binding-recyclerview-db7c40d9f0e4
What’s Left?
All the boilerplate from the RecyclerView is now handled
and all you have left to do is the hard part: loading data off the UI
thread, notifying the adapter when there is a data change, etc.
Android Data Binding only reduces the boring part.

Android kotlin data-binding with fragments error:cannot find symbol import com.example.***.databinding.FragmentUserBindingImpl;

I am relatively new to kotlin and trying to build a project with data binding in some fragments.I have a fragment named UserFragment with a Recyclerview in it like this:
class UserFragment : Fragment() {
private lateinit var binding: FragmentUserBinding
private lateinit var viewModel: UserListViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding=DataBindingUtil.inflate(inflater,R.layout.fragment_user, container, false)
binding.userRecycler.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
viewModel = ViewModelProviders.of(this).get(UserListViewModel::class.java)
viewModel.errorMessage.observe(this, Observer {
errorMessage -> if(errorMessage != null) showError(errorMessage) else hideError()
})
binding.mViewModel=viewModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//start add activity
val i= Intent(activity,AddUserActivity::class.java)
userFab.setOnClickListener(View.OnClickListener {
startActivity(i)
})
}
private fun showError(#StringRes errorMessage:Int){
errorSnackbar = Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_INDEFINITE)
errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener)
errorSnackbar?.show()
}
private fun hideError(){
errorSnackbar?.dismiss()
}
}
and the xml layout file fragment_user.xml looks like this:
<?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" xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="mViewModel"
type="com.example.***.ui.User.UserListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="#+id/userDateEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="#string/pick_date"
android:background="#drawable/roundededittext"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/userRecycler"
android:layout_width="293dp"
android:layout_height="475dp" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="#+id/userDateEditText"
app:adapter="#{viewModel.getUserListAdapter()}"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/userFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_marginBottom="48dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"/>
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mutableVisibility="#{viewModel.getLoadingVisibility()}"
android:id="#+id/userProgressBar" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="140dp" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.804"
app:layout_constraintVertical_bias="0.499"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
there is also a similar adapter class and item_user.xml:
class UserListAdapter : RecyclerView.Adapter<UserListAdapter.ViewHolder>() {
private lateinit var userModelList:List<UserModel>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListAdapter.ViewHolder {
val binding: ItemUserBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_user, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: UserListAdapter.ViewHolder, position: Int) {
holder.bind(userModelList[position])
}
override fun getItemCount(): Int {
return if(::userModelList.isInitialized) userModelList.size else 0
}
fun updateUserList(userModelList:List<UserModel>){
this.userModelList = userModelList
notifyDataSetChanged()
}
class ViewHolder(private val binding: ItemUserBinding):RecyclerView.ViewHolder(binding.root){
private val viewModel = UserViewModel()
fun bind(userModel: UserModel){
viewModel.bind(userModel)
binding.viewModel =viewModel
}
}
}
the item-user.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>
<variable
name="viewModel"
type="com.example.***.ui.MyUser.UserViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="#+id/user_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textStyle="bold"
app:mutableText="#{viewModel.getUserTitle()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/user_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:mutableText="#{viewModel.getUserDesc()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/user_title" />
</android.support.constraint.ConstraintLayout>
</layout>
note that databinding has been enabled in the gradle
and the very important issue here is that in both my fragment and the adapter
this line:
binding.viewModel =viewModel
reports a type mismatch like this:
Type mismatch.
Required:MyUser.UserListViewModel?
Found:com.example.***.ui.MyUser.UserListViewModel
and when i build my project the error is as follows:
error: cannot find symbol
import com.example.***.databinding.FragmentUserBindingImpl;
The first error is pretty explicit: binding.viewModel expects a nullable UserListViewModel? and get a non-nullable UserListViewModel(see kotlin null safety doc).
You can try something like this to get rid of it:
Declare your viewModel as
private var viewModel: UserListViewModel? = null
and set your binding this way:
viewModel?.let{binding.viewModel = it}
Concerning the second error, your declarations seems fine, but sometimes the Android Studio's cache get corrupted, try Invalidate Caches/Restart, it may help.
Type mismatch.
Required:MyUser.UserListViewModel?
Found:com.example.***.ui.MyUser.UserListViewModel
Basically the error is telling that your
binding.viewModel //is a nullable type and there for it expects a nullable
//type to be assigned as well
So just turn your view model into a nullable type by addind the ? simbol after its delacarion (note late init types do not allow nullable types). try it like this
private var viewModel: UserListViewModel? = null
About the second error data binding library need to compile in order to autogenerate the binding class, just rebuild the project and this error will be gone.
Please feel free to use this template as a base to avoid all that boilerplate code
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.CLASS)
annotation class ContentView(#LayoutRes val id: Int)
fun ViewGroup.inflate(#LayoutRes layoutId: Int,
addContainer: Boolean = false): View {
return LayoutInflater.from(context).inflate(layoutId,this,addContainer)
}
#Suppress("UNCHECKED_CAST")
abstract class BaseFragment<Model : ViewModel, Binding : ViewDataBinding> : Fragment() {
/**
* Data binding class variable all view elements declared on the
* Xml file will be available within this instance if a view model
* Is required for the xml to work we will need to bind it on #onBindViewModel
*/
protected lateinit var binding: WeakReference<Binding?>
/**
* Fragments view model according to MVVM android architecture
* Each fragment class should have one , in order to facilitate
* Live Data and Binding library features, tho we can skip it
*/
protected lateinit var viewModel: WeakReference<Model?>
/**
* Here is where most likely you will get not null data , both binding and
* view model references can be destroyed by garbage collector
* If this application reaches low memory levels
*
* This optional method is used to bind the required view model inside the
* Xml file, this is optional to use tho is recommended
* Bind them by calling the view model binding.customViewModel = viewModel
*/
protected open fun onBindViewModel(viewModel: Model?, binding: Binding?) {}
/**
* There will be the occasion where custom params will be needed on view model's
* Constructor in this case will want to override the default creation #see ViewModelFactory
*/
protected open fun onCreateViewModel(modelType: Class<Model>): Model? = if (modelType != ViewModel::class.java)
ViewModelProviders.of(requireActivity()).get(modelType) else null
/**
* Here we will inherit view model and binding values based on the class
* Parameters and store them in global variables so any class extending
* From Base activity has access to binding and view model instances by default
*/
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val layout = this.javaClass.annotations.find { it.annotationClass == ContentView::class } as ContentView?
?: throw RuntimeException("You annotate this class with #ContentView and provide a layout resource")
container?.let { binding = WeakReference(DataBindingUtil.bind(it.inflate(layout.id))!!) }
?: run { binding = WeakReference(DataBindingUtil.bind(inflater.inflate(layout.id, null))) }
viewModel = WeakReference(
onCreateViewModel(
(this.javaClass.genericSuperclass
as ParameterizedType).actualTypeArguments[0] as Class<Model>
)
)
setHasOptionsMenu(true)
onBindViewModel(viewModel.get(), binding.get())
return binding.get()?.root
}
}
And just use it like this (see how much boiler plate code is gone)
#ContentView(R.layout.fragment_user)
class UserFragment: BaseFragment<UserListViewModel, FragmentUserBinding> {
override fun onBindViewModel(viewModel: UserListViewModel?, binding: FragmentUserBinding?) {
binding.viewModel = viewModel
}
}

Categories

Resources