Faustino Gagneten
Faustino Gagneten

Reputation: 2774

MVVM - MutableLiveData of Custom Model not been updating into ViewModel with databinding and always is null

Summary:

When I try to send to the repository a custom Model, the MutableLiveData is null. I think it is because of the observer of the MutableLiveData.

Please, read to the end.

ViewModel

class LoginViewModel @Inject constructor(
        private val repository: MyRepository) : ViewModel() {


    var loginModel: MutableLiveData<LoginModel>

    init {
        loginModel = MutableLiveData<LoginModel>()
    }

    fun loadUser(): LiveData<Response<Custom<Token>>> {

        return repository.login(loginModel.value!!)
    }
}

As you can see here I have a MutableLiveData

My LoginModel is something like this

data class LoginModel(var user : String, var password : String)

A fragment was defined using databinding and binding with the ViewModel above.

login_fragment.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"
    tools:context="com.app.example.view.BaseActivity">

    <data>
        <variable
            name="viewModel"
            type="com.app.example.view.login.LoginViewModel" />

    </data>

    <android.support.constraint.ConstraintLayout
        android:id="@+id/activityMain"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/bg_login">

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="80dp"
            android:layout_marginLeft="16dp"
            android:layout_marginRight="16dp"
            android:layout_marginTop="80dp"
            app:cardCornerRadius="7dp"
            app:cardElevation="22dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center|top"
                android:layout_marginTop="60dp"
                android:text="@string/bienvenido_text"
                android:textAllCaps="true"
                android:textSize="20sp" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_margin="20dp"
                android:orientation="vertical">

                <android.support.design.widget.TextInputLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <EditText
                        android:id="@+id/etEmail"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="20dp"
                        android:layout_marginRight="20dp"
                        android:cursorVisible="true"
                        android:text="@{viewModel.loginModel.user}"
                        android:gravity="center|start|bottom"
                        android:hint="@string/email_text"
                        android:inputType="textEmailAddress"
                        android:maxLength="50"
                        android:paddingBottom="10dp"
                        android:textSize="18sp" />

                </android.support.design.widget.TextInputLayout>

                <android.support.design.widget.TextInputLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:passwordToggleEnabled="true">

                    <android.support.design.widget.TextInputEditText
                        android:id="@+id/etPassword"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="20dp"
                        android:layout_marginRight="20dp"
                        android:layout_marginTop="30dp"
                        android:cursorVisible="true"
                        android:gravity="center|start|bottom"
                        android:hint="@string/contrasenia_text"
                        android:text="@{viewModel.loginModel.password}"
                        android:inputType="textPassword"
                        android:maxLength="50"
                        android:paddingBottom="10dp"
                        android:textSize="18sp" />

                </android.support.design.widget.TextInputLayout>

                <Button
                    android:id="@+id/btnServerLogin"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:layout_margin="15dp"
                    android:padding="10dp"
                    android:text="@string/ingresar_text"
                    android:textSize="18sp" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="bottom|center"
                android:layout_marginBottom="40dp"
                android:orientation="horizontal">

            </LinearLayout>
        </android.support.v7.widget.CardView>

    </android.support.constraint.ConstraintLayout>

</layout>

Using Databinding, I've implemented the relationship between etEmail and the property user of the Model UserModel and also I've implemented the relationship between etPassword and the property password

when the user click on btnServerLogin, the LoginFragment will execute the following code

binding.btnServerLogin.setOnClickListener {
                loginViewModel!!.loadUser().observe(this, Observer<Response<Custom<Token>>> { this.handleResponse(it) })
}

And here is the problem.

kotlin.KotlinNullPointerException

There reason is because of loginModel.value!! is null

fun loadUser(): LiveData<Response<Custom<Token>>> {

        return repository.login(loginModel.value!!)
}

The MutableLiveData's value is always null. I though It would change at the same time you were typing in your EditText of email or passoword.

Here is the code of the LoginFragment OnActivityCreated in which I've initialized the ViewModel

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    setHasOptionsMenu(true)
    DeviceUtils.setTranslucentStatusBar(activity!!.window, R.color.colorPrimaryDark)

    loginViewModel = ViewModelProviders.of(this, viewModelFactory)
            .get(LoginViewModel::class.java)
    binding.setLifecycleOwner(this)
    binding.viewModel = loginViewModel


    binding.btnServerLogin.setOnClickListener {
                mPostsViewModel!!.loadUser().observe(this, Observer<Response<RespuestaModel<JwtTokenModel>>> { this.handleResponse(it) })
    }

    return
}

What am I doing wrong? Shall I need to add an specific Observer?

Thanks

Upvotes: 6

Views: 3396

Answers (3)

Faustino Gagneten
Faustino Gagneten

Reputation: 2774

Thanks to @communistWatermelon, I was able to resolve a piece of the problem but it wasn't enough. Here is the complete solution:

On one hand we have to initialize the value property of the MutableLiveData as @communistWatermelon said so:

var loginModel: MutableLiveData<LoginModel> = MutableLiveData()

init {
    loginModel.value = LoginModel()
}

On the other hand, we need to set properly the binding in the layout

In the case of @{variable.field} the binder will generate a piece of code to get the value by calling variable.getField(). Note that the binder implicitly prefix the “get” word if it is not provided. So @{variable.getField()} is an explicit valid option.

In the case of @={variable.field} the binder will create the same code as before, but with additional code for getting the value from the View and setting it back to your object

After reading that, we need to change the way of the binding

android:text="@{viewModel.loginModel.password}"
android:text="@{viewModel.loginModel.user}"

to

android:text="@={viewModel.loginModel.password}"
android:text="@={viewModel.loginModel.user}"

EDIT

For those who didn't resolve using the above explanation, try registering the lifecycleOwner in your fragment with the following line:

binding.setLifecycleOwner(this)

Good coding!

Upvotes: 10

Jacques.S
Jacques.S

Reputation: 3593

This has happened to me as well, except what had happened is I used MutableLiveData on my ViewModel without registering my fragment/activity as LifeCycleOwner to the ViewDataBinding.

The Question would not experience this issue because

    binding.setLifecycleOwner(this)

This may not be relevant to the question, but if anyone else lands here like I did, double check that you do the setLifecycleOwner on your binding.

Upvotes: 4

communistWatermelon
communistWatermelon

Reputation: 166

MutableLiveData<LoginModel>() doesn't initialize the value property of the MutableLiveData object. You'll likely need to set a value in the init call, like this:

init {
    loginModel = MutableLiveData<LoginModel>()
    loginModel.value = LoginModel()
}

Because you're not setting a value, doing so would cause the `NullPointerException at

return repository.login(loginModel.value!!)

Upvotes: 1

Related Questions