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.
Related
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
I am trying to find out how to bind both the list items, and the selected value/index of an Android Spinner (I am pretty new to Android / Kotlin)
I have the following
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.app.Modes" />
<variable
name="viewModel"
type="com.example.app.MainActivityViewModel" />
</data>
....
<Spinner
android:layout_row="17"
android:layout_column="2"
android:id="#+id/spinner1"
android:layout_width="1200px"
android:entries="#{viewModel.devicesDescriptions}"
app:selectedValue="#={viewModel.devicePosition}"
android:layout_height="wrap_content"
android:background="#android:drawable/btn_dropdown"
android:spinnerMode="dropdown"/>
and in the View Model
val devicesDescriptions = ObservableArrayList<String>()
var devices = listOf<MidiDeviceInfo>()
fun setFoundDevices(d: MutableList<MidiDeviceInfo>) {
devices = d
for (dev in devices)
devicesDescriptions.add(dev.toString())
}
By trial and error I could set just strings to the Spinner items (the MidiDeviceInfo would have been better, but string will do)
However, I cannot get a binding to get the selectedItem to work.
I have tried many things, but with the above, I have the error
Found data binding error(s):
[databinding] {"msg":"Cannot find a getter for \u003candroid.widget.Spinner app:selectedValue\u003e that accepts parameter type \u0027java.lang.String\u0027\n\nIf a binding adapter provides the getter, check that the adapter is annotated correctly and that the parameter type matches.","file":"app\\src\\main\\res\\layout\\activity_main.xml","pos":[{"line0":334,"col0":4,"line1":343,"col1":39}]}
Anyone know a way to do this?
Try using android:selectedItemPosition="#={viewModel.devicePosition}" instead of app:selectedValue="#={viewModel.devicePosition}".
I'm currently using bindings to dynamically set the texts of various text views using the android view models. At the moment the view models look something like this:
class MyViewModel(
resources: Resources,
remoteClientModel: Model = Model()
) : ObservableViewModel() {
init {
observe(remoteClientModel.liveData) {
notifyChange()
}
fun getTextViewTitle(): String = when {
someComplicatedExpression -> resources.getString(R.string.some_string, null)
else -> resources.getString(R.string.some_other_string)
}
}
And the xml layout:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="my.app.signature.MyViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{viewModel.textViewTitle}"
android:textAlignment="center"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
However I would like to remove the "resources: Resources" that is injected into the view model, since the resources are coupled with the Activity. The code now simply returns the string resource id instead:
fun getTextViewTitle(): Int = when {
someComplicatedExpression -> R.string.some_string
else -> R.string.some_other_string
}
Hence I've removed the activity dependency. The compiler thinks this is fine but it crashes in runtime with the following exception: android.content.res.Resources$NotFoundException: String resource ID #0x0.
This happens when trying to attach the lifeCycleOwner to the binding using:
override fun onActivityCreated(savedInstanceState: Bundle?) {
// Some more code....
binding.lifecycleOwner = activity
// Some more code....
I'm not sure how to remove the resource dependency from the view model without having it crash in runtime.
EDIT:
For clarification: The ObservableViewModel in my example is the very same one as the one found here:
https://developer.android.com/topic/libraries/data-binding/architecture
Used to perform notifyChange.
The issue here is the code is trying to call textView.setText(0) which results in an error since there is no string resource with id 0x0. This is happening because getTextViewTitle() return an Int and the view binding functionality will make it default as 0 (when initializing).
https://developer.android.com/topic/libraries/data-binding/expressions#property_reference
From the docs
Avoiding null pointer exceptions
Generated data binding code automatically checks for null values and avoid null pointer exceptions. For example, in the expression #{user.name}, if user is null, user.name is assigned its default value of null. If you reference user.age, where age is of type int, then data binding uses the default value of 0.
Maybe something like this could work,
android:text='#{viewModel.textViewTitle == 0 ? "" : #string{viewModel.textViewTitle}}'
or
android:text='#{viewModel.textViewTitle, default=""}'
To solve this simply make a context available in the view, so that you can call context.getString(...) in your view.
<data>
<import type="androidx.core.content.ContextCompat" />
<variable
name="viewModel"
type="my.application.path.SomeViewModel" />
</data>
<....
....
android:text="#{context.getString(viewModel.textResource)}"
...
/>
Just convert your int value to String to avoid this crush
android:text='#{String.valueOf(viewModel.profile.walletBalance)}'
In some cases your binding variable itself can be null
<data>
<variable
name="viewModel"
type="SomeViewModel"
/>
</data>
<TextView
android:text="#{viewModel == null ? "" : viewModel.textViewTitle}"
/>
I'm having this problem for a while, hope someone can help me
I'm trying to implement two-way data binding for the first time, but I'm facing a weird problem.
The problem here is that every time that I set a value of a live data from my view model, the UI changes, but when I change the edit text value on the UI, it does not reflect on the view model live data value.
Seems like the two-way data binding is working one-way only, when the value is set from the view model
I have my view model, something like this:
class CreateAssignmentViewModel(): ViewModel() {
val assignment = MutableLiveData<String>()
}
then my activity:
class CreateAssignmentActivity: AppCompatActivity() {
private val createViewModel: CreateAssignmentViewModel by viewModel()
private lateinit var viewBinding: ActivityCreateAssignmentBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = DataBindingUtil.setContentView(this, R.layout.activity_create_assignment)
viewBinding.lifecycleOwner = this
viewBinding.createViewModel = createViewModel
}
}
and then on my activity xml:
<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="createViewModel"
type="com.marcelo.tasks.assignments.create.CreateAssignmentViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".assignments.create.CreateAssignmentActivity">
<EditText
style="#style/Base.EditText"
android:text="#{createViewModel.assignment}" />
</LinearLayout>
</layout>
Actually you didn't set two way data binding in your view. You have to use #={} for two way data binding. Check below:
Use
<EditText
android:text="#={createViewModel.assignment}" />
Instead of
<EditText
android:text="#{createViewModel.assignment}" />
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.