Android MVVM ClickListener best practice - android

As I read sunflower and many projects we have the multi clean ways for implementing a ClickListener
Binding clicklistener in view (Activity/fragment)
Creating a separated variable for clickListener on XML and call it on the constructor
Creating a static method and calling it from XML
Creating a viewModel with two uses (model and method) and passing the ViewModel class directly to XML and calling the method on our object
1
binding.addPlant.setOnClickListener {
navigateToPlantListPage()
}
2
<data>
<variable
name="clickListener"
type="android.view.View.OnClickListener"/>
<variable
name="plant"
type="com.google.samples.apps.sunflower.data.Plant"/>
</data>
then
init {
binding.setClickListener { view ->
binding.viewModel?.plantId?.let { plantId ->
navigateToPlant(plantId, view)
}
}
}
3
companion object{
#JvmStatic
#BindingAdapter("bind:setSubjectText")
fun setSubjectText(textView: TextView, string: String){
val mTxt = "Subject: ${string}"
textView.text = mTxt
}
}
4
<data>
<variable
name="viewmodel"
type="com.android.example.livedatabuilder.LiveDataViewModel" />
</data>
as a model
<TextView
android:id="#+id/current_weather"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{viewmodel.currentWeather}"
tools:text="Tokyo" />
in the same XML as a ViewModel method
<Button
android:id="#+id/refresh_button"
android:layout_width="match_parent"
android:layout_height="56dp"
android:onClick="#{() -> viewmodel.onRefresh()}"
android:text="#string/refresh_label" />
As you see each of them has some pros and cons. For instance for each theme:
Messy view
When we want to call multi-object in XML, the XML will be messy
In the large-scale program, we will engage with many static methods
Messy ViewModel
Question: which of them is the best practice for implementing clicklistener, especially in large-scale programs with some fragments by many clickable objects?

In my opinion, none of the methods you mentioned were not good anymore, and any new project could use Jetpack Compose. In this fashion, you do not need fragments, XMLs, binding. etc

Related

Data Binding Compiler cannot find custom binding adapter provided

I have made a binding adapter available statically inside my Fragment which basically change my button appearance from "Stop" to "Play" and vice-versa.
companion object {
#BindingAdapter("playState")
fun Button.setPlayState(item: UIState) {
item.let {
if (it.isPlaying) {
setText("Stop")
setBackgroundColor(ContextCompat.getColor(context, R.color.colorStop))
} else {
setText("Play")
setBackgroundColor(ContextCompat.getColor(context, R.color.colorPlay))
}
}
}
}
Here is my layout file. I have provided a data class for it.
<?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>
<!-- stuff here -->
<variable
name="viewmodel"
type="com.mypackage.ui.ViewModel"/>
<variable
name="uistate"
type="com.mypackage.ui.UIState" />
</data>
<!-- layout, buttons, and more stuff here. Just pay attention to this following button -->
<Button
android:id="#+id/play_button"
android:layout_width="150sp"
android:layout_height="75sp"
android:layout_marginTop="20sp"
android:onClick="#{() -> viewmodel.onPlayClicked()}"
android:text="#string/play_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/minus_layout"
app:layout_constraintVertical_bias="0.026"
app:playState="#{uistate}"/>
</layout>
UIState itself is pretty self-explanatory.
data class UIState(var isPlaying: Boolean)
and the () -> viewmodel.onPlayClicked() flips the Boolean at UIState.
After compiling, Data Binding Compiler throws this error:
Cannot find a setter for <android.widget.Button app:playState>
that accepts parameter type 'com.mypackage.ui.UIState'
I have tried:
Rebuilding the project by removing .gradle folder
Looking for answer here and here.
Removed #JvmStatic annotation at the extension function
Moved the extension function to top level instead of Fragment's companion object.
I think you missed to add kotlin plugin in your gradle
apply plugin: 'kotlin-kapt'
You don't have to use #JvmStatic because you are using Kotlin extension feature.
You need to add the view reference as a paramater to your BindingAdapter method.
#BindingAdapter("playState")
fun setPlayState(button:Button,item: UIState) {
//do your work here
}
Your namespace
xmlns:app="http://schemas.android.com/apk/res-auto"
is wrong for custom binding adapters. Please use the namespace
xmlns:app="http://schemas.android.com/tools"
since app:playState is not in the namespace you have given its not working properly

Android mvvm livedata and databinding

I am experimenting the new architecture components from Google trying to achieve more reactive code using ViewModel, LiveData and DataBinding.
Basically my idea around ViewModel is to have only one field of type Model(user for the record since we are representing a user profile scree). So my ViewModel class is :
class ViewModel : ViewModel() {
var model = MutableLiveData<User>()
and my Model class is :
class User(var name: String, var lastName: String, var age: Int)
In my layout file, I am trying to bind the fields on my Model into the view using DataBinding plugin. The problem is that since my ViewModel has a MutableLiveData<User> I can't access (from xml binding) the fields inside the User class (name, lastName...).
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.github.andromedcodes.mvvmtutorial.ViewModel" />
</data>
<RelativeLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/text_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="#{viewModel.user.name}"/>
</RelativeLayout>
</layout>
Is it even possible to do that? And which is better, having a ViewModel with separate Fields (String, Int, Whatever...) or re-using a Model?
You can see my repository where I did everything using DataBinding and MVVM pattern . Just visit https://github.com/xyarim/android-architecture

Databinding and included layouts: Cannot find setter attribute for onClick

I'm trying to set an OnClickListener for an <include>d layout, but receive a data binding error at compile time stating that data binding "Cannot find the setter for attribute 'android:onClick' with parameter type android.view.View.OnClickListener".
Context here is that I'm using data binding to inflate the included layout, so that I can pass values into it from a viewModel that I've bound to the including layout.
I've tried various syntax for the data binding expression:
#{viewModel::onClickFunction}
#{viewModel.onClickFunction}
#{() -> viewModel.onClickFunction()}
#{(view) -> viewModel.onClickFunction()}
#{_ -> viewModel.onClickFunction()}
I've tried all of the above with onClickFunction as a function, and also as an OnClickListener object.
Other related questions on Stack Overflow seem to solve this issue by cleaning the project to regenerate the databinding files, but that hasn't worked for me.
Relevant code snippets below:
viewModel
class MyViewModel() {
val onClickFunctionAsAListener: OnClickListener = object:OnClickListener{
override fun onClick(v: View?) {
//Do something
}
}
fun onClickFunction() {
//Do something
}
}
Including layout
<layout>
<data>
<variable name="viewModel" type="full.package.name.MyViewModel"/>
</data>
<LinearLayout>
<include
layout="#layout/included_layout"
android:onClick="#{viewModel.onClickListener}"
app:customAttribute="#{`someText`}/>
</LinearLayout>
</layout>
Included layout
<layout>
<data>
<variable name="customAttribute" type="String"/>
</data>
<TextView
layout="#layout/included_layout"
android:text="#{customAttribute}"/>
</layout>
It seems that you can't actually assign an OnClick handler to an <include> tag directly. I managed to get it to work by adding another variable to IncludedLayouts data binding class, and then assigning the OnClickListener to IncudedLayouts root view in XML.
After the changes, my files looked like this:
viewModel
class MyViewModel() {
val onClickFunction: OnClickListener = object:OnClickListener{
override fun onClick(v: View?) {
//Do something
}
}
}
Including layout
<layout>
<data>
<variable name="viewModel" type="full.package.name.MyViewModel"/>
</data>
<LinearLayout>
<include
layout="#layout/included_layout"
app:onClick="#{viewModel.onClickListener}"
app:customAttribute="#{`someText`}/>
</LinearLayout>
</layout>
Included layout
<layout>
<data>
<variable name="customAttribute" type="String"/>
<variable name="onClick" type="android.view.View.OnClickListener"/>
</data>
<TextView
layout="#layout/included_layout"
android:text="#{customAttribute}"
android:onClick="#{onClick}"/>
</layout>
include tag does not support onClick method directly. While the selected answer is correct, instead of passing onClickLister to include layout (or having custom #BindingAdapter, which is also another solution), I would just wrap my include inside a ViewGroup and onClick on ViewGroup.
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="#{()-> viewModel.yourFunction()}">
<include layout="#layout/custom_layout"/>
It's a workaround, but works as charm.

Databind LiveData to CustomView

I have a ViewModel with a List auf MutableLiveData<Data> in my Fragment Layout I set the data variable of my CustomView with one of the data elements from the List.
This works fine when it first loads but it doesn't update when I change a value in my data object.
Not really sure how to do this, until now I just used two-way data binding with EditText and MutableLiveData for example.
CustomView Layout:
<data>
<variable
name="data"
type="androidx.lifecycle.LiveData<Data>"/>
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardBackgroundColor="#{data.color}"
app:cardCornerRadius="16dp">
Class:
var data: MutableLiveData<Data>? = null
set(value) {
binding.data = value
}
Fragment Layout:
<data>
<variable
name="viewModel"
type=".ViewModel" />
</data>
<CustomView
.
.
.
app:data="#{viewModel.data[1]}" />
The reason for the update only happening the first time the screen is loaded is that the XML is used to inflate the View and then the initial item is used and set to the CustomView.
Then when the item in the list is updated, it does not trigger an update in the CustomView.
What you might be looking for is #BindingAdapter
#BindingAdapter("enableButton")
internal fun Button.enableButton(enabled: Boolean?) = enabled?.let { isEnabled = it } ?: also { isEnabled = false }
And then using it in the following way:
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button Text"
app:enableButton="#{viewModel.observeStatus()}" /> // <- Observes Boolean
A good walk-through might be at the following link: BindingAdapter
Note: The example is only for a Boolean observation, but it can simply be changed to match whatever object is observed.

Referencing properties of Observable class in Android Data Binding layout

What is the type of the Observable class property which getter is annotated as #Bindable in the Android Data Binding framework?
For example, let the Observable class be defined as follows:
class Localization() : BaseObservable() {
var translation: (key: String) -> String by Delegates.observable(defaultTranslation) { _, _, _ ->
notifyPropertyChanged(BR.translation)
}
#Bindable get
}
The layout XML will be then something like this:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="translation"
type="WHAT IS THE TYPE OF TRANSLATION?" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{translation.invoke(stringKey)}" />
</FrameLayout>
</layout>
The question is, what to put in the type attribute of variable "translation".
I've tried:
type="kotlin.jvm.functions.Function1<String, String>"
It compiles, but the TextView is not updated when translation property changes.
I can achieve the desired behavior by introducing localization variable in the layout XML and then calling localization.translation.invoke() in the binding expression. I am just not comfortable with this and want to know if I can reference translation directly.
The Localization extends BaseObservable while Function1 is not observable at all. So using the Localization gives you an interface for observing the changes to the properties.
If you bind the translation, it's a simple field that gets set. If you want to update it, you'd have to call setTranslation() again.

Categories

Resources