1

I am trying to create a simple "Log in" screen using the MVVM pattern. I have two-way data binding between my View and a Model class, but what does that leave for the ViewModel to do?

Originally I thought that I would not even have a Model class and my ViewModel class would have the properties to do two-way data binding with the View, but the ViewModel class already extends a class necessary for it to be inflated in the Fragment, and therefore can not extend BaseObservable to allow two-way data binding.

I think I am confused in general on the how these components are supposed to interact with each other, or what I need/dont need.

My Fragment (View)

public class LoginFragment extends Fragment {

    private LoginViewModel mViewModel;
    public static LoginFragment newInstance() {
        return new LoginFragment();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mViewModel = ViewModelProviders.of(this).get(LoginViewModel.class);
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                         @Nullable Bundle savedInstanceState) {
        LoginFragmentBinding binding = LoginFragmentBinding.inflate(inflater, container, false);
        binding.setViewModel(mViewModel); // mViewModel is null here...
        binding.setLoginInfo(new LoginInfo());
        return binding.getRoot();
    }
}

And some of my login_fragment.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>
    <variable name="viewModel" type="login.ui.LoginViewModel"/>
    <variable name="loginInfo" type="login.ui.model.LoginInfo" />
</data>
<android.support.constraint.ConstraintLayout
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".login.ui.LoginFragment">

    <EditText
        android:id="@+id/input_password"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:autofillHints="password"
        android:ems="10"
        android:hint="@string/hint_password"
        android:inputType="textPassword"
        android:text="@={loginInfo.password}"
        app:layout_constraintBottom_toTopOf="@id/button_sign_in"
        app:layout_constraintEnd_toEndOf="@id/input_username"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="@id/input_username"
        app:layout_constraintTop_toBottomOf="@id/input_username" />

    <EditText
        android:id="@+id/input_username"
        android:layout_width="350dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="285dp"
        android:autofillHints="username"
        android:ems="10"
        android:hint="@string/hint_username"
        android:inputType="textEmailAddress"
        android:text="@={loginInfo.username}"
        app:layout_constraintBottom_toTopOf="@id/input_password"
        app:layout_constraintEnd_toStartOf="@id/guideline"
        app:layout_constraintStart_toStartOf="@id/guideline"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button_sign_in"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/hint_sign_in"
        android:onClick="@{()-> viewModel.onSignInClicked(loginInfo)}"
        app:layout_constraintBottom_toTopOf="@id/button_create_account"
        app:layout_constraintEnd_toEndOf="@id/input_password"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="@id/input_password"
        app:layout_constraintTop_toBottomOf="@id/input_password" />

My ViewModel class

public class LoginViewModel extends ViewModel {
    // Want to bind this to a button in the XML, but
    // the mViewModel instance in the LoginFragment isnt assigned
    public void onSignInClicked(LoginInfo info) {
        Log.i("Username", info.getUsername());
        Log.i("Password", info.getPassword());
        // TODO: Actual log in attempt
    }
}

My LoginInfo (Model) class

public class LoginInfo extends BaseObservable {
    private String username = "";
    private String password = "";

    @Bindable
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        /*Listener will repeatedly call setPassword() every time it is notified,
         avoid infinite loops*/
        if (!this.username.equals(username)) {
            Log.i("Username", username);
            this.username = username;
            notifyPropertyChanged(BR.username);
        }
    }

    @Bindable
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
         /*Listener will repeatedly call setPassword() every time it is notified,
         avoid infinite loops*/
        if (!this.password.equals(password)) {
            Log.i("Password", password);
            this.password = password;
            notifyPropertyChanged(BR.password);
        }
    }
}
Dane Lowrey
  • 170
  • 1
  • 10

3 Answers3

2

I think you are a bit confused about MVVM.

In MVVM (Model-View-ViewModel) there isn't direct communication between model class and view class. I remember this by writing it as M-VM-V. That means, your view(fragment, activity, xml) will communicate with VM and vice-versa. And your model(data classes, pojo) will communicate with viewModel class and vice-versa.

So, you shouldn't use both view model and model in xml. Keep reference of model in view-model class to set data and get data. Use view-model having observables to do data binding with xml.

Also, you should never write viewModel.getModel().getSomething() anywhere in any view class. Instead create a method in view model which returns such value. This is all about how easier it would be for you to replace your model class without changing a single line in any of your view classes.

Gaurav Chauhan
  • 376
  • 3
  • 14
  • Yes I agree with this, but which class should then inherit from `BaseObservable` so that it can do two-way data binding? Or will Observable fields let me do this? – Dane Lowrey Oct 19 '18 at 21:27
  • @Dane yes, you can use observable fields instead of Base observable to achieve the same. Just define ObservableField in viewModel and use it for two way binding. You can also use InverseBindingAdapter – Gaurav Chauhan Oct 20 '18 at 01:53
0

All the MV* patterns are meant to facilitate loose coupling between your Interaction layer and Business logic.

In MVVM your View should know about your Model, but never relay on it.

It's the VM job to take the data from the Model and parse in a way that the View can show to the user.

Nidrax
  • 372
  • 3
  • 12
0

The divisions between the various layers in MVVM is a bit of a grey area.

Personally, the way I split it is

  • Model - just the application data, nothing more, nothing less - usually as simple POCO classes.

  • View - the user interface, no business logic here.

  • ViewModel - everything else. The primary role of the ViewModel is the grand controller and data provider for a View. This can take the form of directly exposing the model object for data-binding, (collections of) other sub-ViewModel objects, commands for the U.I. to trigger processing, (injected) services such as external data storage and retrieval.

More details on my recent blog posts - Model / ViewModel. Yes I'm aware that this question is tagged as Android, and my blog is primarily focused on WPF, but the general principles still apply.

Peregrine
  • 4,287
  • 3
  • 17
  • 34