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
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 am trying to use last features from android - Kotlin, mvvm, architecture components, jetpack, databinding, one activity - many fragments approach with new navigation graph, but I am struggling with handling UI events in Fragments
In activity it is simple with kotlin-android-extensions
In XML I create a Button like this:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="clicked"/>
and in Activity I just write
fun clicked(view : View){
}
That's perfect, but unfortunately does not work in Fragment. Yes it is possible to still handle event in Activity and send it to fragment but that's ugly.
Next option is to use an interface,
public interface MyClickCallback{
void onLoginButtonClick();
}
implement it in fragment.
In xml it looks like this:
<variable
name="clickCallback"
type="com.test.MyClickCallback" />
then in fragment's onCreateView I have to set clickCallback to the fragment and finally I can use it
#Override fun onLoginButtonClick() {
}
Problem I have with this is to declare interface and on each new UI event enhance this interface and update fragment which implements it
Next option is RxView.clicks what looks really great with all its features. For example:
RxView.clicks(mSearchBtn)
.throttleFirst(2, TimeUnit.SECONDS)
.map(aVoid -> mSearchEdit.getText().toString().trim())
.filter(s -> !TextUtils.isEmpty(s))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
KeyBoardUtil.closeKeybord(mSearchEdit,
SearchActivity.this);
showSearchAnim();
clearData();
content = s;
getSearchData();
});
Problem here is that I have to bind it to the UI component - mSearchBtn. I do not want this :-). I do not want to have any UI component in fragment unless I really have to. I am always communicating with layout file via variables declared in layout like this
<data>
<variable
name="items"
type="java.util.List" />
</data>
I would love to bind it to variable declared in the XML which is set in Button
android:onClick="myclick"
But I did not find the way how to do it.
Anybody can help me maybe with other simple and nice options ?
In your databinding layout create a variable that is of type View.OnClickListener:
<variable
name="onClickListener"
type="android.view.View.OnClickListener" />
Set it to your View like this:
<View
...
android:onClickListener="#{onClickListener}"
... />
In your Fragment create the onClickListener and set it to the variable:
binding.onClickListener = View.OnClickListener {
/* do things */
/* like getting the id of the clicked view: */
val idOfTheClickedView = it.id
/* or get variables from your databinding layout: */
val bankAccount = binding.bankAccount
}
Or in Java:
binding.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
/* do things */
/* like getting the id of the clicked view: */
Int idOfTheClickedView = view.getId();
/* or get variables from your databinding layout: */
Object bankAccount = binding.getBankAccount()
}
});
it is simple with kotlin-android-extensions
It is indeed simple, but you are currently not using it to its fullest potential.
Setting click listeners in Kotlin is very easy, look:
fun View.onClick(clickListener: (View) -> Unit) {
setOnClickListener(clickListener)
}
And now thanks to synthetic imports in Kotlin-Android-Extensions:
<Button
android:id="#+id/myButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="#string/click_me"/>
and
import kotlinx.synthetic.blah.* // something like that
// Activity:
override fun onCreate(bundle: Bundle?) {
super.onCreate(bundle)
setContentView(R.layout.blah)
myButton.onClick {
// handle click event
}
}
// Fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?) = inflater.inflate(R.layout.blah, container, false)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
myButton.onClick {
// handle click event
}
}
But if you really want to use databinding and layouts for this, then set the callback lambda and inside the databinding layout file.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="activity" type="com.acme.MainActivity"/>
</data>
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/btnOpenSecondView"
android:text="Click me for second view!"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:onClick="#{(v) -> activity.startNextActivity(v)}" />
</RelativeLayout>
</layout>
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 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.