2

I have a question about two-way data binidng implemented by MutableLiveData in ViewModel Class for and EditText.

If I define a LoginViewModel Class for a user, which is consisted of User, email and password as follows:

class LoginViewModel : ViewModel() {
    val user = MutableLiveData<User>()
}

and

data class User(var email: String, var password: String)

when I rotate the phone (configuration changes occurs), the data entered will be gone.

<?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.udacity.shoestore.screens.login.LoginViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/fragment_horizontal_margin"
        android:paddingTop="@dimen/fragment_vertical_margin"
        android:paddingRight="@dimen/fragment_horizontal_margin"
        android:paddingBottom="@dimen/fragment_vertical_margin"
        tools:context=".screens.login.LoginFragment">

        <TextView
            android:id="@+id/email_text"
            style="@style/title_style"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/layout_margin"
            android:layout_marginStart="@dimen/medium_margin"
            android:layout_marginEnd="@dimen/medium_margin"
            android:text="@string/str_email"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/email_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/medium_margin"
            android:layout_marginEnd="@dimen/medium_margin"
            android:autofillHints=""
            android:hint="@string/str_email_hint"
            android:inputType="textEmailAddress"
            android:text="@={loginViewModel.user.email}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/email_text" />

        <TextView
            android:id="@+id/password_text"
            android:layout_width="0dp"
            style="@style/title_style"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/medium_margin"
            android:layout_marginStart="@dimen/medium_margin"
            android:layout_marginEnd="@dimen/medium_margin"
            android:text="@string/str_password"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/email_edit" />

        <EditText
            android:id="@+id/password_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/medium_margin"
            android:layout_marginEnd="@dimen/medium_margin"
            android:autofillHints=""
            android:hint="@string/str_password_hint"
            android:inputType="textPassword"
            android:text="@={loginViewModel.user.password}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/password_text" />

        <Button
            android:id="@+id/sign_up_button"
            style="@style/Widget.AppCompat.Button.Colored"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:layout_marginEnd="@dimen/small_margin"
            android:text="@string/str_sign_up"
            app:layout_constraintBaseline_toBaselineOf="@+id/sign_in_button"
            app:layout_constraintEnd_toStartOf="@+id/sign_in_button"
            app:layout_constraintStart_toStartOf="@+id/password_edit" />

        <Button
            android:id="@+id/sign_in_button"
            style="@style/Widget.AppCompat.Button.Colored"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:layout_marginStart="@dimen/small_margin"
            android:layout_marginTop="@dimen/medium_margin"
            android:layout_marginBottom="@dimen/medium_margin"
            android:text="@string/str_sign_in"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="@+id/password_edit"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/sign_up_button"
            app:layout_constraintTop_toBottomOf="@+id/password_edit"
            app:layout_constraintVertical_bias="0.524" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

I wonder if there is something wrong about defining the User data class or using it in ViewModel or something else, but it doesn't work.

On the other hand, if I define the elemnts of user seperately in ViewModel, it works:

class LoginViewModel : ViewModel() {
    val email = MutableLiveData<String>()
    val password = MutableLiveData<String>()
}

and of course some changes in xml:

 android:text="@={loginViewModel.email}"

and

 android:text="@={loginViewModel.password}"

any idea?

My ShoeStore project in GitHub

Parissa Kalaee
  • 606
  • 1
  • 9
  • 17
  • In your first approach, `email` and `password` are regular `String`s. I don't see why changing those would cause a new value to be emitted on `user`. – Michael Mar 21 '22 at 09:28
  • @Michael Do you mean I should change like this? ```data class User(var email: MutableLiveData, var password: MutableLiveData)``` I did it and it made no changes. I thougt when the User class itself is MutableLiveData, it could be emitted for its properties' changes. – Parissa Kalaee Mar 21 '22 at 11:08
  • 1
    If you want to emit a new value on `user`, then you must set a new value on `user`. For example, you could have `email` and `password` as `MutableLiveData` members of the ViewModel, with two-way data binding, and make `user` a `MediatorLiveData` with `email` and `password` as its sources. – Michael Mar 21 '22 at 11:14
  • @Michael thanks for your comment, I didn't get why I need that, but I changed the line to : ``` val user = MediatorLiveData() ``` and nothing happened. I still lose the data entered for email and password, when I rotate the phone (configuration changes) – Parissa Kalaee Mar 21 '22 at 14:59
  • 1
    If that's the only thing that you did then it won't solve anything. A `MediatorLiveData` emits data based on changes in other `LiveData`s, and you have to explictly add those other `LiveData`s as sources for the `MediatorLiveData`. – Michael Mar 21 '22 at 15:07
  • Actually I changed three files, User.kt (to define MutableLiveData for email and password), the related xml file and the LoginViewModel as follows: ```class LoginViewModel : ViewModel() { val user = MediatorLiveData() // val email = MutableLiveData() // val password = MutableLiveData() }``` you may check the project on GitHub, I added the resource link. – Parissa Kalaee Mar 21 '22 at 15:18

2 Answers2

0

I use this exact same implementation in my app.

Did you set binding lifecycleOwner in your activity or fragment ?

Espera Awo
  • 40
  • 5
  • I set this in LoginFragment.kt: ```binding.lifecycleOwner = this``` – Parissa Kalaee Mar 21 '22 at 10:40
  • and your answer is interesting to me, because I have another data class in my whole project that lifecycleOwner is the activity and there is no problem related to losing data during configuration changes. But there I set the lifeCycleOwner to Activity, because the viewModel is shared between two fragment, and it is not the case in LoginFragment. Is that the solution? Why can't I set the lifeCycleOwner to Fragment, and view model class memebers as live data in two-way databinding? – Parissa Kalaee Mar 21 '22 at 15:03
  • 1
    I always use binding.lifecycleOwner = viewLifecycleOwner in fragments. Using this is discouraged in fragments, it may be the cause of your issues. – Espera Awo Mar 21 '22 at 18:55
0

When using viewmodels, this is actually how I do it.

class LoginViewModel : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> get() = _user
}

In my fragment onCreateView

override
fun onCreateView(...){
   val binding = FragmentBinding.inflate(...)

   binding.lifecycleOwner = viewLifecycleOwner
   binding.viewModel = viewModel

   ...
}

I'll check your project on GitHub and let you know if anything... Sorry for the late reply. I was busy playing with Jetpack Compose ;)

Espera Awo
  • 40
  • 5
  • Don't mind the implementation in this viewmodel because it usually applies to only data that is consumed by the UI. i.e one way databinding – Espera Awo Mar 21 '22 at 19:44
  • thanks for your comment, I changed the lifeCycleOwner to what you said, but nothing happened. what is your User data class? it is really a big help, if you can check the code in Github – Parissa Kalaee Mar 22 '22 at 08:53