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);
}
}
}