I have quick question about my code. I'm writing an app using Android MVVM with LiveData. I want to create loading layout which is will be included in many views. Main goal is to have ability of passing live data representing if layout should be visible and what text info should be displayed with progress bar.
So far I created loading indicator layout, and definded two variables "indicatorVisibility" and "progressText". In attached code one of values is commented out. I created also BindingAdapters to set visibility and text on controls.
This is my layout with progress bar
<data>
<variable
name="indicatorVisibility"
type="android.arch.lifecycle.LiveData"/>
<!--<variable-->
<!--name="progressText"-->
<!--type="android.arch.lifecycle.LiveData"/>-->
</data>
<android.support.constraint.ConstraintLayout
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/layout_loading_background"
>
<ProgressBar
android:id="#+id/progressBar2"
style="?android:attr/progressBarStyle"
android:layout_width="#dimen/layout_loading_progress_bar_size"
android:layout_height="#dimen/layout_loading_progress_bar_size"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/textView7"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--android:text="#{progressText}"-->
<TextView
android:id="#+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="TextView"
android:textColor="#color/layout_loading_text"
android:textSize="#dimen/layout_loading_text_size"
app:layout_constraintBottom_toBottomOf="#+id/progressBar2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="#+id/progressBar2"
app:layout_constraintTop_toTopOf="#+id/progressBar2" />
</android.support.constraint.ConstraintLayout>
This is how i include it in fragment layout
<include layout="#layout/layout_loading_info"
app:indicatorVisibility="#{viewModel.isBusy}"
/>
And those are my bind adapters:
#BindingAdapter("android:visibility")
fun getVisibility(view: View, liveData: LiveData<Boolean>){
liveData.observe(view.getLifecycleOwner(), Observer {
view.visibility = if(it == true) View.VISIBLE else View.INVISIBLE
})
}
#BindingAdapter("app:text")
fun getText(view: TextView, liveData : LiveData<Int>)
{
liveData.observe(view.getLifecycleOwner(), Observer {
it?.let{
view.text = view.context.resources.getString(it)
}
})
}
So far I tried passing simple types like Integer and it works. The problem lays in LiveData. Even when I don't use variables inside included layout I get error (error message tells nothing).
I saw similar stack task [here] : Applying databinding adapter to include tag but they passed the whole viewModel, which is not a flexible enough solution for me.
I think you use the wrong name-space; for data-binding that should be bind:
<include
layout="#layout/layout_loading_info"
bind:indicatorVisibility="#{viewModel.isBusy}"/>
The data-type is LiveData<Boolean>; therefore you'd need to import LiveData and Boolean, in order to use them in a variable definition. The data-binding should look about like this:
<data class=".databinding.LiveDataBinding">
<import type="android.arch.lifecycle.LiveData"/>
<import type="java.lang.Boolean"/>
<variable name="indicatorVisibility" type="LiveData<Boolean>"/>
</data>
bind:viewModel="#{viewModel}" might in general be better than binding single values.
Related
I want to start using viewBinding in our project but the mere addition of the configuration results in a compile error:
android {
buildFeatures {
dataBinding true
viewBinding true // new line and only change
}
results in:
e: /home/leo/StudioProjects/android-wallet/mbw/build/generated/source/kapt/btctestnetDebug/com/mycelium/wallet/DataBinderMapperImpl.java:37: error: cannot find symbol
import com.mycelium.wallet.databinding.FragmentBequantAccountBindingImpl;
^
symbol: class FragmentBequantAccountBindingImpl
location: package com.mycelium.wallet.databinding
Cannot find a setter for <com.mycelium.wallet.databinding.ItemBequantSearchBinding app:visibility> that accepts parameter type 'int'
If a binding adapter provides the setter, check that the adapter is annotated correctly and that the parameter type matches.
The offending code is:
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.mycelium.bequant.market.viewmodel.AccountViewModel" />
</data>
...
<include
android:id="#+id/searchBar"
layout="#layout/item_bequant_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="#{viewModel.searchMode ? View.VISIBLE : View.GONE}" removing="this line fixes compilation"
app:layout_constraintTop_toBottomOf="#id/hideZeroBalance" />
Changing the offending line to any of
android:visibility="#{viewModel.searchMode ? `visible` : `gone`}"
app:visibility="#{viewModel.searchMode ? View.VISIBLE : View.GONE}"
results in similar errors.
I read I might have to define a BindingAdapter but why and where?
I tried adding
#BindingAdapter("visibility")
fun setVisibility(target: View, visible: Boolean) {
target.visibility = if (visible) View.VISIBLE else View.GONE
}
to AccountFragment which inflates above xml file changing the xml to
android:visibility="#{viewModel.searchMode}"
but this appears to have no effect.
Both fragment_bequant_account.xml and item_bequant_search.xml use androidx.constraintlayout.widget.ConstraintLayout instead of androidx.constraintlayout.ConstraintLayout.
I tried to put a #BindingAdapter into the AccountViewModel as suggested here but with no success.
I had the same problem in my project. I used databinding in my code and had dataBinding true in the gradle. As soon as I added viewBinding true I got the same error pointing to the xml line android:visibility="#{viewModel.searchMode ? View.VISIBLE : View.GONE}"
To fix, I added the tools:viewBindingIgnore="true" attribute to the root view of a certain layout file so that layout is ignored while generating binding classes.
You can see documentation on the tools:viewBindingIgnore="true" attribute at https://developer.android.com/topic/libraries/view-binding#data-binding
The problem is in the viewBinding trying to create the binding class of the layout in the include.
It seems that the binding class created for the main layout(dataBinding) manages the included layout in a different way when viewBinding = true and don't understand it's attrs
As James said tools:viewBindingIgnore="true" is the solution, in this case it must be in the included layout(layout="#layout/item_bequant_search").
Every reused layout must have tools:viewBindingIgnore="true" to avoid this issues
The problem is with this statement
app:visibility="#{viewModel.searchMode ? View.VISIBLE : View.GONE}"
it evaluates and pass View.VISIBLE or View.GONE to the binding adapter method,But
#BindingAdapter("visibility")
fun setVisibility(target: View, visible: Boolean)
As your method signature says it expects a boolen but evaluation results in int i.e. either View.VISIBLE or View.GONE.
The issue can be solved by removing the evaluation and passing the boolean directly.
app:visibility="#{viewModel.searchMode}"
I assument viewModel.searchMode is a boolean variable.
Lets you create a kotlin file Named BindingAdapters.kt
Paste this method directly there
#BindingAdapter("visibility")
fun setVisibility(target: View, visible: Boolean) {
target.visibility = if (visible) View.VISIBLE else View.GONE
}
else lets say you have a class BindingAdapters in a file BindingAdapters.kt
class BindingAdapters{
companion object{
#BindingAdapter("visibility")
#JvmStatic// it is important
fun setVisibility(target: View, visible: Boolean) {
target.visibility = if (visible) View.VISIBLE else View.GONE
}
}
}
I got similar error and this is my solution:
You only add tag '<layout.../layout>' to all include layout like this:
In main layout:
<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="viewId"
type="Integer" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/bg">
<include
android:id="#+id/img_no_data"
layout="#layout/layout_no_data"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="#{viewId==0? View.VISIBLE: View.GONE}"
app:layout_constraintBottom_toTopOf="#+id/btn_camera"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
In include layout: add tag '<layout...' too:
<layout>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/imageView2"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="#dimen/_16sdp"
android:layout_marginEnd="#dimen/_16sdp"
android:src="#drawable/bg_no_data"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="986:817"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Done! Hope this help you.
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
I am working on new project and just started using Databinding. I heard people talking abaout code reduction.I have a RecyclerView in fragment. I have a simple ConstraintLayout in which I have 3 TextViews. This is in my Adapter for RecyclerView.
That aditional +" nazv" is just a sample and I could use String Extension.
fun bind(item: Mkdo) {
binding.txtMkdoNaziv.text = item.nazv+" nazv"
binding.txtMkdoPost.text = item.post
binding.txtmkdoPostNaziv.text = item.postNaziv
}
ConstraintLayout for single RecyclerView row (deleted positioning properties)
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintRowMkdo"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txtMkdoNaziv"/>
<TextView
android:id="#+id/txtMkdoPost"/>
<TextView
android:id="#+id/txtmkdoPostNaziv"/>
</androidx.constraintlayout.widget.ConstraintLayout>
This works just fine.
If I want to convert it to Databinding I have to add:
fun bind(item: Mkdo) {
binding.mkdo=item
binding.executePendingBindings()
}
And BindingAdapter
#BindingAdapter("setNazv")
fun TextView.setNazv(item:Mkdo?){
item?.let {
text="${item.nazv} nazv"
}
}
ConstraintLayout for single RecyclerView row (deleted positioning properties) with Databinding
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintRowMkdo"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txtMkdoNaziv"
app:setNazv="#{mkdo}""/>
<TextView
android:id="#+id/txtMkdoPost"
android:text="#{mkdo.post}"/>
<TextView
android:id="#+id/txtmkdoPostNaziv"
android:text="#{mkdo.postNaziv}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
This makes constraintRowMkdo nonreusable and in my opinion adds more work than without it.
Also if I have some string formatting I need to have it on two places string Extension and BindingAdapter. I can reuse Extension but it is still on who places.
What are your toughts on this?
Am I missing something?
Are there any aditional benefits of using it?
Thanks
I know Android MVVM, LiveData and DataBinding. But, I have a scenario in which I have many input UI fields such as Email, Password, Confirm Password and etc. I can map those fields with ViewModel.
public class LoginViewModel extends ViewModel {
public MutableLiveData<String> email = new MutableLiveData<>();
public MutableLiveData<String> password = new MutableLiveData<>();
.
.
.
}
I bound this LoginViewModel with the following 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="loginViewModel"
type="viewModel.LoginViewModel" />
</data>
<RelativeLayout
android:id="#+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
tools:context=".view.MainActivity">
<EditText
android:id="#+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textEmailAddress"
android:text="#={loginViewModel.email}" />
<EditText
android:id="#+id/editText2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/editText"
android:ems="10"
android:inputType="textPassword"
android:text="#={loginViewModel.password}" />
</RelativeLayout>
</layout>
Actually, I have more UI fields so which ideal approach should I follow? Whether to declare the exact same LiveData in ViewModel based on my UI. Like 10 UI fields should have 10 LiveData in ViewModel.
Your view model should expose 10 data fields if your UI can show it (don't overload UI). It's how MVVM works. But! You should expose differed types of fields depends on field behaviour:
LiveData for read-only fields (e.g. TextView)
MutableLiveData for mutable fields, two way databinding (e.g. EditText)
non LiveData type for constant (read-only) data. If you know data not changed during view model life cycle, you can expose data without LiveData. In this case your data will be binded once when you setup view model variable.
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.