Android mvvm livedata and databinding - android

I am experimenting the new architecture components from Google trying to achieve more reactive code using ViewModel, LiveData and DataBinding.
Basically my idea around ViewModel is to have only one field of type Model(user for the record since we are representing a user profile scree). So my ViewModel class is :
class ViewModel : ViewModel() {
var model = MutableLiveData<User>()
and my Model class is :
class User(var name: String, var lastName: String, var age: Int)
In my layout file, I am trying to bind the fields on my Model into the view using DataBinding plugin. The problem is that since my ViewModel has a MutableLiveData<User> I can't access (from xml binding) the fields inside the User class (name, lastName...).
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.github.andromedcodes.mvvmtutorial.ViewModel" />
</data>
<RelativeLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/text_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="#{viewModel.user.name}"/>
</RelativeLayout>
</layout>
Is it even possible to do that? And which is better, having a ViewModel with separate Fields (String, Int, Whatever...) or re-using a Model?

You can see my repository where I did everything using DataBinding and MVVM pattern . Just visit https://github.com/xyarim/android-architecture

Related

Android two-way data binding working one way only

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}" />

2 way data binding error for a livedata variable with proper xml binding

I have a loginViewModel for my login Activity, in activity_login.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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="loginViewModel"
type="com.example.test.ui.login.LoginViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/login_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="#+id/mobile_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:editable="true"
android:ems="10"
android:hint="#string/mobile_number_string"
android:inputType="phone"
android:textAlignment="center"
android:text="#={loginViewModel.phoneNumber}"
app:layout_constraintBottom_toTopOf="#+id/otp_input"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And in my loginViewModel i have defined my livedata as
class LoginViewModel : ViewModel(){
private val _phoneNumber = MutableLiveData<String>()
val phoneNumber : LiveData<String>
get() = _phoneNumber
}
Now, while building I am getting the following error
The expression \u0027loginViewModelPhoneNumber.getValue()\u0027 cannot be inverted, so it cannot be used in a two-way binding\n\nDetails: There is no inverse for method getValue, you must add an #InverseMethod annotation to the method to indicate which method should be used when using it in two-way binding expressions
All articles that I am reading suggesting this way to implement.
Can someone tell me what am I doing wrong here?
Unfortunately for two-way data binding you need to use MutableLiveData.
You should remove private on _phoneNumber.
Then change xml to use it android:text="#={loginViewModel._phoneNumber}".
change from
private val _phoneNumber = MutableLiveData<String>()
to
public val _phoneNumber = MutableLiveData<String>()
You are binding phoneNumber which is a LiveData that does not have any interface for writing value.
Consider removing phoneNumber, and using Kotlin-based approach, with just a public property
There are several issues with the code.
you try to use val phoneNumber that is not mutable in two way binding, thus is has only getter so binding class can only read value from field, to receive data from the UI - binding class wants to use setter, but there is no setter present, thus the phoneNumber won't change.
for the property that binds phoneNumber you try to use LiveData which is not mutable - you should use MutableLiveData in case you need it to be able to change.
if you want to listen to the changes of phoneNumber you need to add LiveData Observer like
phoneNumber.observe{
val value = it
}
Hope it helps.

Android Studio 3.4 Kotlin BindingAdapter crashes the ap

I have been trying to use BindingAdapter in Android Studio 3.4 (the last update) with Kotlin for days now and nothing seems to work.
I first tried with the following tutorial: https://codelabs.developers.google.com/codelabs/android-databinding/#7
And it was outputing an error as soon as I reached the 8th step.
Furthermore I tried the simple example possible with an Empty Application, a single Activity, a single ViewModel, and a single BindingAdapter. Here is the XML code.
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewmodel"
type="com.example.testbindingadapter.DataViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:greetings="#{viewmodel.name}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="#+id/textView"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Now here is the ViewModel with the BindingAdapter
class DataViewModel : ViewModel() {
private val _name = MutableLiveData<String>()
val name : LiveData<String> = _name
init {
_name.value = "Amath"
}
}
#BindingAdapter("greetings")
fun setName(view: TextView, text: String) {
view.text = "Welcome, $text"
}
I have also enabled dataBinging in my Graddle. I added apply plugin: 'kotlin-kapt' as suggested in the following thread Cannot find the setter for attribute in Data binding. At first I had an error msg:Cannot find the setter for attribute databinding subsequently the error disappeared, but the app simply crashed.
Can you help ?
You never set the viewmodel into databinding:
binding.viewmodel = viewModel

How to use companion objects on xml layout?

I am trying to use a companion object property inside the layout but the compiler doesn't recognise it.
Kotlin Class
class MyClass {
companion object {
val SomeProperty = "hey"
}
}
XML Layout
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fancy="http://schemas.android.com/tools">
<data>
<import type="package.MyClass"/>
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClass.Companion.SomeProperty}"/>
</layout>
And I got this error:
e: java.lang.IllegalStateException: failed to analyze: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
****/ data binding error ****msg:Could not find accessor package.MyClass.Companion.SomeProperty file:/path/to/my/layout.xml loc:21:67 - 21:103 ****\ data binding error ****
at org.jetbrains.kotlin.analyzer.AnalysisResult.throwIfError(AnalysisResult.kt:57)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules(KotlinToJVMBytecodeCompiler.kt:138)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:154)
...
Caused by: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
****/ data binding error ****msg:Could not find accessor package.MyClass.Companion.SomeProperty file:/path/to/my/layout.xml loc:21:67 - 21:103 ****\ data binding error ****
at android.databinding.tool.processing.Scope.assertNoError(Scope.java:112)
at android.databinding.annotationprocessor.ProcessDataBinding.doProcess(ProcessDataBinding.java:101)
at android.databinding.annotationprocessor.ProcessDataBinding.process(ProcessDataBinding.java:65)
...
I've tried to use companion instead of Companion, but no luck.
Is it possible to use companion objects on xml layout with databinding? How can I proceed? Thanks in advance for any help :)
In order to access Companion object attributes and methods, it is NOT required to have an instance of the Parent object.
Companion object are already instantiated, therefore you can access the instance directly.
Instead of using <import> (which is the natural translation from Java), we need to use <variable>, because we actually want to use the (already instantiated) Companion object into our XML Layout.
Import your Companion object as follow
Given Kotlin class:
package com.example.project
class MyViewModel {
companion object {
// it is only working with val and var
// const val wouldn't work
val MAX_LENGTH = 10
}
}
Layout:
<data>
<!-- Declare your "variable" that hold the Companion object itself -->
<variable name="myViewModelStatic" type="com.example.project.MyViewModel.Companion" />
</data>
<!-- then use the myViewModelStatic to access "static" properties of MyViewModel -->
<EditText
...
android:maxLength="#{ myViewModelStatic.MAX_LENGTH }"
/>
</layout>
Fragment:
class MyFragment {
...
onViewCreated(...) {
// now bind the companion object to the variable declared in the XML
binding.myViewModelStatic = TransferUseCase.Companion
}
...
}
You can get rid of Companion keyword if you annotate your method/proprty with #JvmStatic
In the XML just add Companion before the name of the field, for example:
In ViewModel
package com.example.project
class MyViewModel {
companion object {
var leText = "text"
}
var leColor = ...
}
In XML
<data>
<import type="android.view.View" />
<variable
name="context"
type="com.example.project.MyViewModel" />
</data>
<TextView
...
android:text="#{context.Companion.leText}"
android:color="#{context.leColor}"/>
To get access to your property, do the following:
Annotate you companion object property with
#JvmStatic
:
class MyClass {
companion object {
#JvmStatic
val SomeProperty = "hey"
}
}
then go ahead and remove 'Companion' from your binding TextView:
Change from
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClass.Companion.SomeProperty}"/>
to
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClass.SomeProperty}"/>
TL;DR:
Replace
android:text="#{MyClass.Companion.SomeProperty}"
with
android:text="#{MyClass.Companion.getSomeProperty()}"
Explanation:
Your problem is that you're trying to reference the Kotlin object exactly as it is named but that's not how the Kotlin compiler will generate the property in Java. Instead, it will convert it using the Java convention of being a "get" function.
You can find out what this name will look like by decompiling the Kotlin Bytecode.
Open the Kotlin class you want to see the bytecode for.
Open Tools > Kotlin > Show Kotlin Bytecode
In the side panel that opens up, click the Decompile button in the top-left.
This will show you the Java equalvalent of the Kotlin class, including the full name of the companion property.
Bonus:
That said, you probably would prefer to reference the field as a property, in which case you can just append the const keyboard to the property declaration.
const val SomeProperty = "hey"
With that the compiler will generate the field as a public static field, outside of the Companion, and you can update your xml to be simply:
android:text="#{MyClass.SomeProperty}"
Which is pretty much how you'd do it in Java.
Hope that helps!
There is another way:
in class
const val SomeProperty = "hey"
class MyClass {}
in XML
<data>
<import type="package.MyClassKt"/>
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClassKt.SomeProperty}"/>
I went through all of this answers and I had to come up with my own solution.
Mark the constant with const val modifiers (const is the key) and just import the "parent" class (MyClass in your case) and you don't need the word Companion
class MyClass {
companion object {
const val SomeProperty = "hey"
}
}
XML
<data>
<import type="package.MyClass"/>
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{MyClass.SomeProperty}"/>
</layout>

Referencing properties of Observable class in Android Data Binding layout

What is the type of the Observable class property which getter is annotated as #Bindable in the Android Data Binding framework?
For example, let the Observable class be defined as follows:
class Localization() : BaseObservable() {
var translation: (key: String) -> String by Delegates.observable(defaultTranslation) { _, _, _ ->
notifyPropertyChanged(BR.translation)
}
#Bindable get
}
The layout XML will be then something like this:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="translation"
type="WHAT IS THE TYPE OF TRANSLATION?" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{translation.invoke(stringKey)}" />
</FrameLayout>
</layout>
The question is, what to put in the type attribute of variable "translation".
I've tried:
type="kotlin.jvm.functions.Function1<String, String>"
It compiles, but the TextView is not updated when translation property changes.
I can achieve the desired behavior by introducing localization variable in the layout XML and then calling localization.translation.invoke() in the binding expression. I am just not comfortable with this and want to know if I can reference translation directly.
The Localization extends BaseObservable while Function1 is not observable at all. So using the Localization gives you an interface for observing the changes to the properties.
If you bind the translation, it's a simple field that gets set. If you want to update it, you'd have to call setTranslation() again.

Categories

Resources