Android DataBinding LiveData - not notify changed in DialogFragment & BottomSheetDialogFragment - android

I have an BottomSheetDialogFragment 's layout like this:
<data>
<variable
name="viewModel"
type="com.sample.MyViewModel" />
</data>
<TextView android:id="#+id/tvValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='#{String.format("%.1f", viewModel.weight)}'/>
<Button android:id="#+id/cmdUpdate"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:onClick="#{() -> viewModel.updateWeight()}"
android:text="#string/update" />
And here is the kotlin code:
// MyViewModel
val weight = MutableLiveData<Double>()
fun updateWeight() {
weight.value?.let {
weight.value = (it + 0.1)
}
}
// BottomSheetDialogFragment bind view model
val myViewModel = ViewModelProviders.of(it, factory).get(MyViewModel::class)
binding.viewModel = myViewModel
// code showing BottomSheet:
val fragment = MyBottomSheetFragment()
fragment.show(fragmentManager, "bottomsheet")
The first time open bottomsheet fragment, it can show the weight value, but when I click on button to update weight, there is nothing happen. From debugger, I can see that the updateWeight method is called, and the weight value is changed, but the TextView is not updated. This also happens on other DialogFragment.
The same code can work if I use normal Fragment
Is there something wrong with DialogFragment & DataBinding?

You need to call
binding.setLifecycleOwner(this)
According to documentation, it allows to update your view when LiveData was changed.

Related

Using switch in recyclerview with databinding

Here is what I am trying to do:
When the switch is toggled in the recyclerview
Update the item (alarm) which was toggled to the switch state (true or false) using my viewModel from the fragment (Passing the ID of the clicked item (alarm) to my fragment and update its status from the switch)
I, personally dont fancy the idea of having a reference of the viewModel in my adapter..but is that the way out..:(?
I am having difficulty implementing this with databinding.
recycler_view_item.xml
<data>
<variable
name="alarm"
type="com.package.appname.Alarm" />
<variable
name="fragment"
type="com.package.appname.ui.fragments.AlarmFragment" />
.................
<com.google.android.material.switchmaterial.SwitchMaterial
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:checked="#{alarm.status}"
android:onCheckedChanged="#{(button, bool)->
fragment.updateAlarmStatus(bool)}"
android:layout_marginEnd="#dimen/rcv_lyt_margin_spacing"/>
</data>
RecyclerViewAdapter.kt
class AlarmAdapter(private val onClickListener: OnAlarmClickListener) : ListAdapter<Alarm, AlarmAdapter.AlarmViewHolder>(AlarmDiffCallback()) {
class AlarmViewHolder private constructor(private val binding:
RcvLytAlarmsBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(alarm: Alarm?){
binding.alarm = alarm
binding.executePendingBindings()
}
}
AlarmFragment.kt
fun updateAlarmStatus(status: Boolean){
//Here I want to use my viewmodel to update the selected alarm like
vieModel.updateAlarmStatus(alarmClicked)
Timber.d(status.toString()) <--- This code is not been called, IDK why
}

Visibility from an ObservableInt in a ViewModel not showing up

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.

Android passing query parameter to viewmodel

I have a view and viewmodel which has 2 functionalities -
1) Clicking a button in view and getting data.
2) A Spinner where you can select an item and ask the viewmodel to get data for that item as a query parameter.
I already implemented the first point like this -
MyView code -
viewModel.onGetDataClicked.observe(this, Observer {
//....
})
My ViewModel code -
private val viewState = MyViewState()
val onGetDataClicked =
Transformations.map(dataDomain.getData(MyAction.GetDataAction)) {
when (it) {
....
}
}
MyAction code -
sealed class MyAction : Action {
object GetDataAction : MyAction()
}
My question is how do I pass the spinner value from view to the viewmodel? Since in viewmodel I have a val onGetDataClicked and not a function.
First you should get the item value in the view itself, after that pass the item value to the required method in the ViewModel and from the ViewModel to the Repository(where you are querying the data from).
// in view
viewModel.onGetDataClicked(item:DataType).observe(this, Observer {
//....
})
//in viewmodel
private val viewState = MyViewState()
val onGetDataClicked:(item:DataType) =
Transformations.map(dataDomain.getData(MyAction.GetDataAction)) {
//you have item here, pass it where you require
when (it) {
....
}
}
Hi you can use the selectedItemPosition attribute from the view and pass position to viewModel, accordingly you can map the item using the position.
layout.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 class="FeedbackBinding">
<variable
name="vm"
type="com.ec.service.ServiceViewModel" />
</data>
<androidx.appcompat.widget.AppCompatSpinner
android:id="#+id/unitAET"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:selectedItemPosition="#={vm.selectedUnitPosition}"
app:entries="#{vm.unitNames}"
app:layout_constraintEnd_toEndOf="#+id/textView10"
app:layout_constraintStart_toStartOf="#+id/textView10"
app:layout_constraintTop_toBottomOf="#+id/textView10" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
the selectedUnitPosition is a MutableLeveData
fragment.kt
In your fragment initialise the vm (viewModel)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
withViewModel<ServiceViewModel>(factory) {
binding.vm = this
}
}

Too many XML data bindings

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

Simple way how to handle UI events inside fragment (onClick, ...)

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>

Categories

Resources