I am using LiveData, DataBinding, and Kotlin in my application.
I defined a Binding Adapter for a RecyclerView like this:
class MainListBindings {
private val TAG = "TasksListBindings"
companion object {
#JvmStatic
#SuppressWarnings("unchecked")
#BindingAdapter("main_items")
fun setItems(recyclerView: RecyclerView, items: MutableLiveData<List<Homeitem>>? = null) {
val adapter: RecyclerMainAdapter = recyclerView.adapter as RecyclerMainAdapter
//prevent use of null list
items?.let {
adapter.swapData(items)
}
}
}
}
and my reyclerView in XML assigned to this bindingAdapter like this:
<android.support.v7.widget.RecyclerView
android:id="#+id/main_recycler"
app:main_items="#{viewmodel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
And in my ViewModel, I used
val items = MutableLiveData<List<Homeitem>>()
to create the list.
But I am getting this error at building Application:
Found data binding errors.
****/ data binding error ****msg:Cannot find the setter for attribute 'app:main_items' with parameter type android.arch.lifecycle.MutableLiveData<java.util.List<project.data.pojo.Homeitem>> on android.support.v7.widget.RecyclerView. file:/home/user/Yara/bazinama_mag/bazinama/app/src/main/res/layout/fragment_main.xml loc:22:24 - 22:38 ****\ data binding error ****
There might be different reasons for this error but in my case, the problem raised up because I didn't add apply plugin: 'kotlin-kapt' And apply plugin: 'kotlin-android-extensions' in my Gradle.
After adding these plugins you have to replaced your annotationProcessors with kapt.
After that, every thing might be going well.
Binding Adapter
#JvmStatic
#BindingAdapter("main_items")
fun setItems(recyclerView: RecyclerView, items: MutableLiveData<List<String>>) {
}
Model
class User : ViewModel(){
lateinit var list: MutableLiveData<List<String>>
}
Bind Data
val items = ViewModelProviders.of(this#FragmentName)
.get(RecyclerViewHelper.User::class.java)
mBinding!!.user = items
Layout
<layout>
<data>
<variable
name="user"
type="com.XXX.view.recylerview.RecyclerViewHelper.User" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/helper_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:main_items="#{user.list}" />
</RelativeLayout>
</layout>
Check if you have any space after the #BindingAdapter's value like this:
#BindingAdapter("sleepDurationFormatted ")
Took me 2hours to find out.
Related
I have a basic Activity with a View and a ViewModel. I am using DataBinding library.
I need to update the visibility of a TextView from the ViewModel.
Here is how I setup my Activity :
class HomeActivity : AppCompatActivity() {
// Helper classes
private var binding : ActivityHomeBinding? = null
private var viewModel = HomeViewModel()
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_home)
binding?.viewModel = viewModel
// ...
viewModel.setNoContact()
}
// ...
}
Here is how my HomeViewModel looks like :
class HomeViewModel {
val contactsListVisibility = ObservableInt(View.VISIBLE)
val loadingVisibility = ObservableInt(View.VISIBLE)
val noContactVisibility = ObservableInt(View.VISIBLE)
// ...
fun setNoContact() {
Log.d(TAG, "Current no contacts visibility = ${noContactVisibility.get()}")
contactsListVisibility.set(View.GONE)
loadingVisibility.set(View.GONE)
noContactVisibility.set(View.VISIBLE)
Log.d(TAG, "Should set no contacts visibility to ${noContactVisibility.get()}")
}
companion object {
private const val TAG = "HomeViewModel"
}
And here is my View :
<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="com.example.ft_hangouts.home_activity.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="#color/cardview_dark_background"
tools:context=".home_activity.HomeActivity">
<!-- ... -->
<TextView
android:id="#+id/no_contact"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/no_contact"
android:textSize="30sp"
android:visibility="#{viewModel.noContactVisibility}"
android:textColor="#color/white"
style="#style/BasicTextStyle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- ... -->
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The path to my ViewModel in the xml layout is correct. The link is done, when I Ctrl + click it, I am brought to my HomeViewModel.
However, the "No Contact" TextView is never showing up.
Here is the result I get :
And here is the result expected :
When looking at the logs, I have :
D/HomeViewModel: Current no contacts visibility = 8
Should set no contacts visibility to 0
So there is no reason the TextView is not displayed. I am forgetting something ?
Thanks.
I have tried adding dummy data to test this. Assuming you have an ArrayList of contacts like this..
val contactList: ArrayList<Int> = arrayListOf()
and I have added dummy data for contacts so you can test yourself. Usually you will get this data from a service or contact app.
HomeViewModel
fun setDummyContact(isAddYes: Boolean){
if(isAddYes){
contactList.add(7673443334)
contactList.add(2233444512)
contactList.add(1233455623)
}
}
You can set data from your HomeActivity like this...
viewModel.setDummyContact(false) // for no contact data
**OR** viewModel.setDummyContact(true) // add contacts -
You can simply do the following to handle Visibility of TextView based on your contactList data.
Add import of View to your datatype
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="com.example.ft_hangouts.home_activity.HomeViewModel" />
</data>
and then add this to your No Contact TextView
android:visibility="#{viewModel.contactList.size() > 0 ? View.GONE : View.VISIBLE}"
So you don't need to add any function or Boolean value to handle visibility. It is more cleaner and better solution because you don't need to additional variable to handle visibility of the textView. You can do the same for any other View you would like to show or hide.
I watched this awesome talk by Florina Muntenescu on KontlinConf 2018 where she talked about how they reshaped their app architecture.
One part of the talk was how they expose a UiModel (not ViewModel) via LiveData from the ViewModel. (watch here)
She made a example similar to this:
class MyViewModel constructor(...) : ViewModel() {
private val _uiModel = MutableLiveData<UiModel>()
val uiModel: LiveData<UiModel>
get() = _uiModel
}
A view declaration for the ViewModel above could be:
<layout>
<data>
<variable
name="viewModel"
type="com.demo.ui.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="#+id/text"
android:text="#={viewModel.uiModel.text}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
She didn't talked about (or I missed it) how they react to property changes within the UiModel itself. How can I execute a function everytime text changes?
When having the text in separate LiveData property within the ViewModel I could use MediatorLiveData for this like:
myMediatorLiveData.addSource(text){
// do something when text changed
}
But when using the approach above the UiModel does not change instead the values of it are changed. So this here doesn't work:
myMediatorLiveData.addSource(uiModel){
// do something when text inside uiModel changed
}
So my question is how can I react on changes inside a UiModel in the ViewModel with this approach?
Thanks for advice,
Chris
I want to summarize my research regarding the topic above.
As #CommonsWars said in the comments above you can implement the field in the UiModel as ObservableFields. But after some hands on I currently prefer the approach described here.
This led me to the following code:
ViewModel:
class MyViewModel constructor(...) : ViewModel() {
val uiModel = liveData{
val UiModel uiModel = UiModel() // get the model from where ever you want
emit(uiModel)
}
fun doSomething(){
uiModel.value!!.username = "abc"
}
}
UiModel
class UiModel : BaseObservable() {
#get:Bindable // puts '#Bindable' on the getter
var text = ""
set(value) {
field = value
notifyPropertyChanged(BR.text) // trigger binding
}
val isValid: Boolean
#Bindable("text") get() { // declare 'text' as a dependency
return !text.isBlank()
}
}
Layout
<layout>
<data>
<variable
name="viewModel"
type="com.demo.ui.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="#+id/text"
android:text="#={viewModel.uiModel.text}" />
<Button
android:id="#+id/sent_btn"
android:enabled="#{viewModel.uiModel.isValid}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I've chosen this approach because of the way we change properties of the UiModel from within a ViewModel.
We can you can set/get the username in the ViewModel by:
fun doSomething(){
uiModel.value!!.username = "abc"
}
fun doSomething(){
uiModel.value!!.username
}
When you implementing it by ObservableFields you have to set/get the username by:
fun doSomething(){
uiModel.value!!.username.set("abc")
}
fun doSomething(){
uiModel.value!!.username.get()
}
You can pick the approach which suites best for your needs!
Hope this helps someone.
Chris
I made a View that I want to reuse across many pages. It contains feedback elements for the user such as a ProgressBar, TextView etc.
Due to high amount of items within, binding all those turns out like this:
<layout ... >
<data>
<variable
name="screenObserver"
type="my.namespace.ScreenStateObserver" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout ... >
<my.namespace.view.ScreenStateView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading="#{screenObserver.isProgressVisible}"
app:errorText="#{screenObserver.errorTxt}"
app:buttonText="#{screenObserver.errorBtnTxt}"
app:errorVisible="#{screenObserver.isTextVisible}"
app:buttonVisible="#{screenObserver.isButtonVisible}"
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I find copy/pasting the whole XML block messy and error-prone. Is there any way to make this simpler ?
ScreenStateObserver is just a interface that I implement in my ViewModel and bind as follows:
override fun onCreateView(...): View? {
val factory = InjectorUtils.provideViewModelFactory()
viewmodel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)
binding = MyFragmentBinding.inflate(inflater, container, false).apply {
screenObserver = viewmodel
}
}
class AtoZViewModel() : ViewModel(), ScreenStateObserver { ... }
interface ScreenStateObserver {
val isProgressVisible : MutableLiveData<Boolean>
val isTextVisible : MutableLiveData<Boolean>
val isButtonVisible : MutableLiveData<Boolean>
// [..]
}
Thanks !
Here is my suggestion to reduce code.
First declare a class like this
interface ScreenState {
class Loading : ScreenState
class Error(val errorMessage: String, val errorButtonText: String) : ScreenState
}
and inside you CustomView it will be
internal class ScreenStateView {
fun setState(state: ScreenState) {
if (state is ScreenState.Loading) {
// show loading
} else {
// hide loading
}
if (state is ScreenState.Error) {
//show {state.errorMessage} and {state.errorButtonText}
} else {
// hide error
}
}
}
using in xml
<my.namespace.view.ScreenStateView
...
app:state="#{screenObserver.screenState}"
...
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" /> // for onButtonClick I think it still better if we keep like this
Hope it help
You can use <include> in data binding layouts. Included layout file can have its own data and variables that you can access from the main binding class as well.
You have to create a layout file(such as layout_state_view.xml that contains your view and data variables relevant to your view:
<layout>
<data>
<variable
name="screenObserver"
type="my.namespace.ScreenStateObserver" />
</data>
<my.namespace.view.ScreenStateView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading="#{screenObserver.isProgressVisible}"
app:errorText="#{screenObserver.errorTxt}"
app:buttonText="#{screenObserver.errorBtnTxt}"
app:errorVisible="#{screenObserver.isTextVisible}"
app:buttonVisible="#{screenObserver.isButtonVisible}"
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" />
</layout>
Now you can include this in your root layout file:
<layout>
<data>
...
</data>
<LinearLayout //Can be any layout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout:="#layout/layout_state_view">
</LinearLayout>
</layout>
Now when you are using binding class, if you root layout file was R.layout.mainActivity then it would look like this:
binding.layoutStateView.setScreenObserver(...)
You can also make a variable in root layout and pass that variable to child layout by using bind tag as mentioned on documentation but since you are looking to reduce code, it would be unnecessary.
Note: Since you only have a single view, you might be tempted to use <merge> tag. Databinding's layout tag does not support merge as a direct child.
Documentation Reference:
https://developer.android.com/topic/libraries/data-binding/expressions#includes
My solution to reduce code is first define class for ScreenStateView(different properties of ScreenStateView in this class) then use it as much times as you needed
So I have a viewHolder with a checkbox
here is my viewModel
#Bindable
var itemIsSelected: Boolean = isSelected
set(value) {
if (field != value) {
field = value
notifyPropertyChanged(BR.itemIsSelected) // this doesn't work
notifyChange() // this one works
}
}
here is my viewHolder class
inner class SpecialityItemViewHolder(val binding: ItemSpecialityFilterBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(specialityItemViewModel: SpecialityItemViewModel) {
binding.viewModel = specialityItemViewModel
binding.executePendingBindings()
this.itemView.setOnClickListener {
binding.viewModel?.let {
it.itemIsSelected = !it.itemIsSelected // this doesn't trigger ui changes
}
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="packagename.ItemViewModel" />
</data>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="#dimen/vertical_margin_small"
android:paddingBottom="#dimen/vertical_margin_small"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<Checkbox
android:id="#+id/checkbox"
android:layout_width="25dp"
android:layout_height="25dp"
android:checked="#={viewModel.itemIsSelected}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
so what happens is that the setting is working properly as in that when i press the checkbox it sets the backing field to the corresponding value
but when i set the backing field (notice code in bind function) it doesn't trigger ui change I know that calling binding.executePendingBindings() would solve the problem but my understanding is that notifyPropertyChanged(BR.itemIsSelected) should not need executePendingBindings call, Actually if i call notifyChange instead everything works properly ( but I presume there is performance issue here as it notifies change for all properties instead )
Your ViewModel class have to extend BaseObservable class and using kotlin you have to use #get:Bindable annotation. If you don't want to use BaseObservable as a parent class then use ObservableField<Boolean>(). You find more information in https://developer.android.com/topic/libraries/data-binding/observability#kotlin
the view-model needs Kotlin annotations, else the annotation processor will ignore it:
class ViewModel : BaseObservable() {
#get:Bindable
var isSelected: Boolean
set(value) {
if (isSelected != value) {
isSelected = value
notifyPropertyChanged(BR.isSelected)
}
}
}
it.isSelected is easier to read than it.itemIsSelected.
I'm trying to use Kotlin extension methods inside Android's databinding. For example; calling an onclick handler. So I've made this code:
posttest_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<data>
<import type="android.view.View"/>
<import type="com.example.test.post.posttest.PostTestItemViewModelExtensionKt" />
<variable
name="viewModel"
type="com.example.test.post.posttest.PostTestItemViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:clickable="true"
android:onClick="#{(view) -> viewModel.clicked(view)}"
>
[...]
PostTestItemViewModel.kt
open class PostTestItemViewModel : ViewModel() {
val postTitle = MutableLiveData<String>()
val postBody = MutableLiveData<String>()
/**
* Binds the required properties/entities to this ViewModel
*/
fun bind(post: Post) {
postTitle.value = post.title
postBody.value = post.body
}
}
PostTestItemViewModelExtension.kt
fun PostTestItemViewModel.clicked(v: View) {
this.postTitle.value = "clicked"
}
So when I place the clicked method inside the viewmodel, it works perfectly the way it should be. However, when I create it as an extension method, I get the following error on compilation:
e: [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
cannot find method clicked(android.view.View) in class ...PostItemViewModel
I've tried different things already, such as changing the android:onclick tag to PostTestItemViewModelExtensionKt instead of viewModel. Unfortunately all the things don't seem to work. So it looks like the extension method is getting generated after the databinding takes place. Is there a way around this or am I still doing something wrong? Or is it just not possible to bind extension methods?
I'm using Kotlin version 1.2.71, gradle 3.2.0 and have the databinding { enabled = true } and kapt { generateStubs = true } added to my .gradle, and have the plugings kotlin-android, kotlin-android-extensions and kotlin-kapt defined.
Unfortunately you can't use extension methods as onClick callbacks.
Extension methods in Kotlin are created as Java static methods while the Android framework is expecting an instance method.
Note that in Android Studio you can decompile the Kotlin classes as Java to see the generated Java code.
So, today(2022) I had the same use case in one of my projects and i was able to figure out a way to implement custom click listeners for android views using data binding and custom adapters.
The use case is :
Click event should not be triggered twice or to prevent accidental clicks from the user
I created a file called ViewExtensions.kt and added the following code
class DebouncingOnClickListener(
private val intervalMillis: Long,
private val doClick: (() -> Unit)
) : View.OnClickListener {
override fun onClick(v: View) {
if (enabled) {
enabled = false
v.postDelayed(ENABLE_AGAIN, intervalMillis)
doClick()
}
}
companion object {
#JvmStatic
var enabled = true
private val ENABLE_AGAIN =
Runnable { enabled = true }
}
}
#BindingAdapter("singleClick")
fun View.setSingleClick(doClick: () -> Unit) =
setOnClickListener(
DebouncingOnClickListener(
intervalMillis = 5000, //5ms delay for click event
doClick = doClick
)
)
The debouncing click is used to defer the click for the given time, and in the xml called the click event like below
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
app:singleClick="#{()->fragment.clicked()}" />
Now I'm able to listen for click events on both fragment and in the viewmodel and the click is deferred for the given amount of time.
Hence the user cannot click the view accidentally multiple times.
References:
https://proandroiddev.com/ensure-single-click-on-android-butterknife-did-it-right-48ef56153c78