Proper implementation of MVVM in Android
When it comes to design patterns in general. You want to keep business logic away from Activities and Fragments.
MVVM and MVP are both really good choices if you ask me. But since you want to implement MVVM. Then i will try to explain a little on how i implement it.
The activity
public class LoginActivity extends BaseActivity {
private LoginActivityViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityLoginBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_login);
NavigationHelper navigationHelper = new NavigationHelper(this);
ToastHelper toastHelper = new ToastHelper(this);
ProgressDialogHelper progressDialogHelper = new ProgressDialogHelper(this);
viewModel = new LoginActivityViewModel(navigationHelper,toastHelper,progressDialogHelper);
binding.setViewModel(viewModel);
}
@Override
protected void onPause() {
if (viewModel != null) {
viewModel.onPause();
}
super.onPause();
}
@Override
protected void onDestroy() {
if (viewModel != null) {
viewModel.onDestroy();
}
super.onDestroy();
}
}
This is a fairly simple activity. Nothing special. I just start with instantiating what my viewModel need. Because i try to keep everything android specific away from it. Everything to ease the writing of tests
Then i just bind the viewmodel to the view.
The view
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.community.toucan.authentication.login.LoginActivityViewModel" />
</data>
<RelativeLayout
android:id="@+id/activity_login_main_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background"
tools:context="com.community.toucan.authentication.login.LoginActivity">
<ImageView
android:id="@+id/activity_login_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="40dp"
android:src="@drawable/logo_small" />
<android.support.v7.widget.AppCompatEditText
android:id="@+id/activity_login_email_input"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_below="@+id/activity_login_logo"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="60dp"
android:drawableLeft="@drawable/ic_email_white"
android:drawablePadding="10dp"
android:hint="@string/email_address"
android:inputType="textEmailAddress"
android:maxLines="1"
android:text="@={viewModel.username}" />
<android.support.v7.widget.AppCompatEditText
android:id="@+id/activity_login_password_input"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_below="@+id/activity_login_email_input"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:drawableLeft="@drawable/ic_lock_white"
android:drawablePadding="10dp"
android:hint="@string/password"
android:inputType="textPassword"
android:maxLines="1"
android:text="@={viewModel.password}" />
<Button
android:id="@+id/activity_login_main_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/activity_login_password_input"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp"
android:background="@drawable/rounded_button"
android:onClick="@{() -> viewModel.tryToLogin()}"
android:paddingBottom="10dp"
android:paddingLeft="60dp"
android:paddingRight="60dp"
android:paddingTop="10dp"
android:text="@string/login"
android:textColor="@color/color_white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/activity_login_main_button"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:onClick="@{() -> viewModel.navigateToRegister()}"
android:text="@string/signup_new_user"
android:textSize="16dp" />
<LinearLayout
android:id="@+id/activity_login_social_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerInParent="true"
android:layout_marginBottom="50dp"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/facebook" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/twitter" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/google" />
</LinearLayout>
<TextView
android:id="@+id/activity_login_social_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/activity_login_social_buttons"
android:layout_centerHorizontal="true"
android:layout_marginBottom="20dp"
android:text="@string/social_account"
android:textSize="16dp" />
</RelativeLayout>
</layout>
Fairly straight forward from the view side. I bind all the specific values the viewModel need to act on the logic it has.
https://developer.android.com/topic/libraries/data-binding/index.html Check the following link to get more knowledge on how the android databinding library works
The ViewModel
public class LoginActivityViewModel extends BaseViewModel implements FirebaseAuth.AuthStateListener {
private final NavigationHelper navigationHelper;
private final ProgressDialogHelper progressDialogHelper;
private final ToastHelper toastHelper;
private final FirebaseAuth firebaseAuth;
private String username;
private String password;
public LoginActivityViewModel(NavigationHelper navigationHelper,
ToastHelper toastHelper,
ProgressDialogHelper progressDialogHelper) {
this.navigationHelper = navigationHelper;
this.toastHelper = toastHelper;
this.progressDialogHelper = progressDialogHelper;
firebaseAuth = FirebaseAuth.getInstance();
firebaseAuth.addAuthStateListener(this);
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onDestroy() {
firebaseAuth.removeAuthStateListener(this);
super.onDestroy();
}
@Override
public void onStop() {
progressDialogHelper.onStop();
super.onStop();
}
public void navigateToRegister() {
navigationHelper.goToRegisterPage();
}
public void tryToLogin() {
progressDialogHelper.show();
if (validInput()) {
firebaseAuth.signInWithEmailAndPassword(username, password)
.addOnCompleteListener(new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (!task.isSuccessful()) {
String message = task.getException().getMessage();
toastHelper.showLongToast(message);
}
progressDialogHelper.hide();
}
});
}
}
private boolean validInput() {
return true;
}
@Override
public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
if (firebaseAuth.getCurrentUser() != null) {
navigationHelper.goToMainPage();
}
}
@Bindable
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
notifyPropertyChanged(BR.username);
}
@Bindable
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}
}
Here is where all the fun happens. I use the helper classes to show and act with the android system. Otherwise i try to keep the logic as clean as possible. Everything is made so it is easier for me to create and test the logic.
Keep note
I bound the username
and password
with the view. So every change made to the EditText will automatically be added to the field. In that way. I do not need to add any specific listener
Hope this small showcase can help you understand a little how you could implement MVVM into your own projects
As for me - MVVM, MVP and other really cool patterns for really cool guys do not have a straightforward receipt/flow. Of course you have a lot of tutorial/recommendations/patterns and approaches how to implement them. But that's actually what all programming is about - you just need to come up with a solution which fits your needs. Depending on your developers vision you can apply a lot of principles to your solution to make it easier/faster to develop/test/support.
In your case I think it is better to move this kind of logic to Fragment transitions(as you have done in setFreeTrialFragment()
), it's more customizable and comfortable to use. But nevertheless if your approach should stay the same - existing one is normal. Actually @BindingAdapter
is more suitable for xml attributes then a direct usage.
As for me - all of the UI logic should reside in the Activity, the main purpose is to separate business logic from UI. Because of that all animations, fragment transactions and so on are handled inside of the activity - that's mine approach. ViewModel - is responsible for notifying the view that something has changed in corresponding model and the view should arrange itself to those changes. In perfect world you should be able to achieve such a popular term as two-way binding, but it is not always necessary and not always UI-changes should be handled inside the ViewModel. As usual, too much MVVM is bad for your project. It can cause Spaghetti code, "where that's from?", "how to recycler view?" and other popular issues. So it should be used only to make life eaisier, not to make everything ideal, because like every other pattern it will make a lot of head ache and someone who will look through your code will say "OVERENGINEERING!!11".
Per request, MVP example :
Here you have some helpful articles :
- Quite simple example.
- Here you have a good description with integration guide.
- First and second part of this articles may be more then helpful.
- This one is short and really descriptive.
Short example(generalized), you should fit it to yours architecture :
Package representation :
Implementation :
Model :
public class GalleryItem {
private String mImagePath;
//other variables/getters/setters
}
Presenter :
//cool presenter with a lot of stuff
public class GalleryPresenter {
private GalleryView mGalleryView;
public void loadPicturesBySomeCreteria(Criteria criteria){
//perform loading here
//notify your activity
mGalleryView.setGalleryItems(yourGaleryItems);
}
//you can use any other suitable name
public void bind(GalleryView galleryView) {
mGalleryView = galleryView;
}
public void unbind() {
mGalleryView = null;
}
//Abstraction for basic communication with activity.
//We can say that this is our protocol
public interface GalleryView {
void setGalleryItems(List<GalleryItem> items);
}
}
View :
public class NiceGalleryView extends View {
public NiceGalleryView(Context context) {
super(context);
}
public NiceGalleryView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// TODO: 29.12.16 do your stuff here
}
And of cource the activity code :
public class GalleryActivity extends AppCompatActivity implements GalleryPresenter.GalleryView {
private GalleryPresenter mPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gallery);
//init views and so on
mPresenter = new GalleryPresenter();
mPresenter.bind(this);
}
@Override
public void setGalleryItems(List<GalleryItem> items) {
//use RecyclerView or any other stuff to fill your UI
}
@Override
protected void onDestroy() {
super.onDestroy();
mPresenter.unbind();
}
}
Also be aware that you even have a lot of different approaches while using MVP. I just want to emphasize that I prefer initialize views in activity and do not pass them out of activity. You can manage this through interface and thats really comfortable not just for development, but even for instrumental tests.