In my last post I wrote that i might have written my first tutorial about how to create an MVVM App with the following characteristics:
- MVVM architecture (Model – View – ViewModel)
- Dagger 2
- DataBinding
- Retrofit
- RxJava
This is a tutorial for Android programmers with experience in creating Android App, so I suppose there the reader has enough experience with Android Studio.
Step 1 – Project creation
The first step is the creation of the project and of all the packages. I will be using Android Studio 3.0 RC 1, but it will work in the same way with previous versions on Android Studio.
So, start Android Studio and then “Start a new Android Studio project”:
- Set the ‘Application name’ you like
- Set the ‘Company domain’ you like
- Set the ‘Project location’ you like
- Adjust the ‘Package name’ as you like
- I will use Java and not Kotlin, so I will let ‘Include Kotlin support’ unchecked
- Select the ‘Phone and Tablet’ target you need (I use API 15)
- I will leave unchecked the following options: ‘Wear’, ‘TV’, ‘Android Auto’ and ‘Android Things’
- Select the activity you need, I will use ‘Empty Activity’
- In the ‘Configure Activity’ check the ‘Generate Layout file’, the option ‘Backwards Compatibility (AppCompat)’ is let to you
- Complete the process pressing ‘Finish’
At this point we have created the standard Android architecture then we need to modify
Step 2 – Packages creation
Now it’s time to organize the packages where all the classes will be created. These are personal choices, so I suggest you to try to understand the reasons of my choices and after this tutorial you can decide to modify my settings to something more suitable for you.
In the root package of the App, create the following packages:
- view: location for all the activity and fragment classes
- model: location for all the data models
- viewmodel: location for all the viewmodels
- service: location for all the classes created at startup and injected
- module: location for all the infrastucture files required by Dagger to create the classes defines in services (If I’ll decide to change the engine to inject the service classes, I have to changes the content of this package and not the service classes)
This is the basic structure of the project and you can add more packages if required.
At this point I usually move the MainActivity class created by the initial project creation form the root package to the ‘view’ pakage (and after the move verify that the ‘name’ property in AndroidManifest.xml for MainActivity is updated accordingly)
Build and run to verify that everything is working
Here is the status of our project:
MVVM architecture (Model – View – ViewModel)- Dagger 2
- DataBinding
- Retrofit
- RxJava
Step 3 – DataBinding
Now it’s time to add the support of dataBinding to reduce the number of lines of code to access the views inside the layouts.
Open the build.gradle (Module: app) file and add the following
dataBinding { enabled true }
inside the ‘Android’ block.
To allow the databinding in the activity/fragment layouts, open the activity_main.xml layout, and after the
<?xml version="1.0" encoding="utf-8"?>
include the tag
android.support.constraint.ConstraintLayout
inside the <layout> tag:
<?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" > <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".view.MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>
Now to check that the dataBinding is working add an ‘id’ to the text view and then build the project
<TextView android:id="@+id/mainTextView"
Now open the .view.MainActivity.java class and inside the onCreate method modify the
setContentView(R.layout.activity_main);
to
ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); activityMainBinding.mainTextView.setText("Databinding is working");
and running the App you will read the text ‘Databinding is working’ in the center of the main activity.
Here is the status of our project:
MVVM architecture (Model – View – ViewModel)- Dagger 2
DataBinding- Retrofit
- RxJava
Step 4 – Linking the View with the ViewModel
The next step is to link the View with the ViewModel. In the Mvvm architecture, the View has access to the ViewModel, but the ViewModel doesn’t know anything about the View so let’s see hot to make it possible with an example.
We will change the content of ‘activity_main.xml’ adding a button on the bottom and the label will display how many time the button has been pressed.
Let’s start with the ViewModel for the MainActivity class, and I will name it ‘MainViewModel’. Inside we will create a method that will be invoked each time the button will be pressed (and the View parameter is expected by the databinding), and the text that will be displayed eact time the button is pressed. Here is the final result:
public class MainViewModel { public static ObservableField<String> counterText = new ObservableField<>(); public MainViewModel() { counterText.set("Button never pressed"); } public void doCounterIncrease(View view) { count++; counterText.set(MessageFormat.format("Button pressed {0} times", count)); } private int count; }
The view has access to the viewmodel, so the ‘doCounterIncrease’ will be called by the view.
The viewmodel has no access to the view, so the viewmodel notify the changes to its properties using ObservableField static properties, and the change will be received by anyone is listening the changes.
Not we need to link the viewmodel with the view creating the viewmodel inside the activity and setting it to the layout file.
Open the ‘main_activity.xml’ file and
- add the <data> tag ad first tag inside <layout>
- create a variable named ‘mainViewModel’ of type ‘xxx.viewmodel.MainViewModel’
- set the text value of the textView to ‘@{mainViewModel.counterText}’
- add a button linked to the bottom of the layout and with text ‘Increment counter’
- set the ‘onClick’ attribute equals to @{mainViewModel.doCounterIncrease}
Note that we are using the ‘counterText’ property of the ViewModel and when updated will update the value of the TextView, and the public method ‘doCounterIncrease’ to notify to the ViewModel that the button has been clicked.
The content of the layout is now:
<?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="mainViewModel" type="uk.co.itmms.mvvm.viewmodel.MainViewModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".view.MainActivity"> <TextView android:id="@+id/mainTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{mainViewModel.counterText}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="Increment counter" android:onClick="@{mainViewModel.doCounterIncrease}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>
If you run the App you will see that the TextView is empty and that when we click the button nothing is happening.
What we are missing is that the variable ‘mainViewModel’ is not istantiated by the view, but it must be injected and so:
- open the MainActivity.java class
- remove the previously added ‘activityMainBinding.mainTextView.setText(“Databinding is working”);’
- in the same position create an instance of MainViewModel and set it to activityMainBinding
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); activityMainBinding.setMainViewModel(new MainViewModel()); } }
You can see that the lifecycle of the MainViewModel class is managed by the activity, and that when declared as varaible it can be injected in the view.
Build and run the App and you will see that the counter will be increased after each pressing of the ‘Increment counter’ button
Note: the biggest achievement so far is that now we can test the MainViewModel class with JUnit using local unit tests and not instrumented tests, and you can test the UI indipendently by how the UI will be rendered.
In this test we can verify that the ‘doCounterIncrease’ invocation will change the ‘counterText’ property
public class MainViewModelTest { private MainViewModel mainViewModel; @Before public void setUp() { mainViewModel = new MainViewModel(); } @Test public void testDoCounterIncrease() { final StringBuilder actual = new StringBuilder(); MainViewModel.counterText.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() { @Override public void onPropertyChanged(Observable observable, int i) { ObservableField<String> observableField = (ObservableField<String>)observable; actual.append(observableField.get()); } }); mainViewModel.doCounterIncrease(null); assertTrue(actual.length() > 0); } }
Here is now the status of our project:
MVVM architecture (Model – View – ViewModel)- Dagger 2
DataBinding- Retrofit
- RxJava
Step 5 – Dagger 2
The introduction of Dagger 2 requires more efforts, but with Dagger in place we will have services available to the ViewModel to dialog with a Restful service and any other service required (MixPanel, database, etc).
Creation of services
Let’s start creating the services that will be used by the ViewModels; all the services are created in the ‘service’ package. We will create two services:
- NetworkingService: contains the list of methods to access the Restful service
- ErrorService: contains the methods to collect the runtime errors and, maybe, notify them to a public service (i.e. Crashlytics).
that we will inject into all the activities, fragments and viewmodels.
Let’s first create the ErrorService.java class in the service package:
public class ErrorService { public ErrorService() { } public void logError(Throwable throwable) { Log.e(TAG, throwable.getMessage(), throwable); } private static final String TAG = "ErrorService"; }
Let’s create the NetworkingService.java class in the service package with the following content:
public class NetworkingService { public interface INetworkingServiceCallback { void successful(); void failed(Throwable throwable); } public NetworkingService(Context context, ErrorService errorService) { this.context = context; this.errorService = errorService; } public void doLogin(String email, String password, INetworkingServiceCallback networkingServiceCallback) { try { // Later it will be changed with the real implementation if (email.contains("@")) { if (networkingServiceCallback != null) { networkingServiceCallback.successful(); } } else { if (networkingServiceCallback != null) { networkingServiceCallback.failed(null); } } } catch (Exception ex) { errorService.logError(ex); } } public void doLogout() { try { // Later it will be changed with the real implementation } catch (Exception ex) { errorService.logError(ex); } } private Context context; private ErrorService errorService; }
In the constructor we are passing a Context parameter just to show how to manage the case when a service requires the use of the Context parameter.
Creation of RuntimeData model
All the runtime variables of the App used by the viewmodels and by the services are saved in the class RuntimeData under the model package
public class RuntimeData { public RuntimeData(Context context) { this.context = context; } private Context context; }
Creation of Base classes
In the view package create the following classes:
- RootActivity.java
- RootFragment.java
and update the MainActivity class to extend the RootActivity
public class RootActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } }
public class RootFragment extends Fragment { @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); } }
In the viewmodel package create the RootViewModel.java class
public class RootViewModel { public RootViewModel() { } }
end then extend the existing MainViewModel from the RootViewModel class
public class MainViewModel extends RootViewModel { public static ObservableField<String> counterText = new ObservableField<>(); public MainViewModel() { super(); counterText.set("Button never pressed"); } public void doCounterIncrease(View view) { count++; counterText.set(MessageFormat.format("Button pressed {0} times", count)); } private int count; }
In the root package create the AppApplication class that extends the Application class
public class AppApplication extends Application { @Override public void onCreate() { super.onCreate(); } }
Remember to add the AppApplication.java in the ApplicationManifest.xml:
<application android:name=".AppApplication" android:allowBackup="true" ....
In a later step we will add to these classes the modules we are going to create.
Import of Dagger 2 library
In the build.gradle App import the Dagger library:
implementation 'com.google.dagger:dagger:2.12' implementation 'com.google.dagger:dagger-android:2.12' annotationProcessor 'com.google.dagger:dagger-android-processor:2.12' annotationProcessor 'com.google.dagger:dagger-compiler:2.12'
or, with older version of Android Studio,
compile 'com.google.dagger:dagger:2.12' compile 'com.google.dagger:dagger-android:2.12' annotationProcessor 'com.google.dagger:dagger-android-processor:2.12' annotationProcessor 'com.google.dagger:dagger-compiler:2.12
and rebuild to import the libraries.
Creation of Dagger modules
The next step is the creation of the scaffolding files to allow the injection of the services created previously. If you haven’t worked before with Dagger this step will not be clear, and in this case I suggest you to learn how dagger works.
Let’s start with the creation of the Modules in the module package.
AppScope.java: scope for the modules
@Scope @Retention(RetentionPolicy.CLASS) public @interface AppScope { }
ContextModule.java
@Module public class ContextModule { public ContextModule(Context context) { this.context = context; } @Provides @AppScope public Context getContext() { return context; } private final Context context; }
DataModule.java: creates all the models used at runtime
@Module public class DataModule { @Provides @AppScope public RuntimeData getRuntimeData(Context context) { return new RuntimeData(context); } }
CommonModule.java: creates the ErrorService
@Module public class CommonModule { @Provides @AppScope public ErrorService getErrorService() { return new ErrorService(); } }
NetworkingModule.java: creates the Networking service and, because it depends on Context and ErrorService, we use the ContextModule and the CommonModule just created
@Module(includes = { ContextModule.class, CommonModule.class }) public class NetworkingModule { @Provides @AppScope public NetworkingService getNetworkingService(Context context, ErrorService errorService) { return new NetworkingService(context, errorService); } }
It’s time to create the Component interface in the module package
@AppScope @Component(modules = { DataModule.class, CommonModule.class, NetworkingModule.class }) public interface AppComponent { void inject(AppApplication appApplication); void inject(RootActivity rootActivity); void inject(RootFragment rootFragment); void inject(RootViewModel rootViewModel); }
The remaining classt to create in the module package is DependencyInjector.java, where the modules are created but, before creating the class, build all the project so that Dagger can create the classes required for this last step.
public class DependencyInjector { public static void initialize(AppApplication appApplication) { appComponent = DaggerAppComponent .builder() .contextModule(new ContextModule(appApplication)) .build(); } public static AppComponent appComponent() { return appComponent; } private DependencyInjector() { } private static AppComponent appComponent; }
Injections using Dagger
It’s time to inject the services to the RootActivity, RootFragment and RootViewModel, but first we need to complete the AppApplication class
public class AppApplication extends Application { @Override public void onCreate() { super.onCreate(); injectDependencies(); } private void injectDependencies() { DependencyInjector.initialize(this); DependencyInjector.appComponent().inject(this); } }
followed by the RootActivity and the RootFragment in the view package
public class RootActivity extends AppCompatActivity { @Inject protected RuntimeData runtimeData; @Inject protected ErrorService errorService; @Inject protected NetworkingService networkingService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); onInject(DependencyInjector.appComponent()); } /** * Performs dependency injection, using the appComponent as the injector. * If an Activity only needs injection into this base class, it does not need to override this method. * However, if an Activity requires extra injections (has one ore more @Inject annotations in it's source code), * then it must override this method, and invoke <code>applicationComponent.inject(this);</code> * * @param appComponent the component being injected */ @SuppressWarnings("WeakerAccess") protected void onInject(AppComponent appComponent) { if (appComponent != null) { appComponent.inject(this); } } }
public class RootFragment extends Fragment { @Inject protected RuntimeData runtimeData; @Inject protected ErrorService errorService; @Inject protected NetworkingService networkingService; @Override public void onActivityCreated(@android.support.annotation.Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); onInject(DependencyInjector.appComponent()); } /** * Performs dependency injection, using the appComponent as the injector. * If a Fragment only needs injection into this base class, it does not need to override this method. * However, if a Fragment requires extra injections (has one ore more @Inject annotations in it's source code), * then it must override this method, and invoke <code>applicationComponent.inject(this);</code> * * @param appComponent injector class */ @SuppressWarnings("WeakerAccess") protected void onInject(AppComponent appComponent) { if (appComponent != null) { appComponent.inject(this); } } }
As you can see, all the descentant classes from RootFragment and RootActivity have access to the RuntimeData, ErrorService and NetworkingService that are automatically injected.
The same will be done in the RootViewModel class in the viewmodel package, and in this way we will avoid to pass the services and the runtime data throw the constructor of the viewmodels
public class RootViewModel { @Inject protected RuntimeData runtimeData; @Inject protected ErrorService errorService; @Inject protected NetworkingService networkingService; public RootViewModel() { onInject(DependencyInjector.appComponent()); } @SuppressWarnings("WeakerAccess") protected void onInject(AppComponent appComponent) { if (appComponent != null) { appComponent.inject(this); } } }
Note: to allow the unit testing, all the methods ‘onInject’ in the RootActivity, RootFragment and RootViewModel classes check that appComponent is not null. The injection of the injected classes don’t work during the unit test, so you have to mock then and inject them in the class under test.
Here is now the status of our project:
MVVM architecture (Model – View – ViewModel)Dagger 2DataBinding- Retrofit
- RxJava
Step 6 – Retrofit and RxJava 2
It’s now time to add the Retrofit library and we will set it to use OkHttp library and gson to serialize/deserialize the json files: in the build.gradle app file we will add the folllowing
// Retrofit implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.squareup.okhttp3:okhttp:$okHttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion" implementation "com.google.code.gson:gson:$gsonVersion"
// RxJava v2 compile "io.reactivex.rxjava2:rxjava:$rxJava2" compile "io.reactivex.rxjava2:rxandroid:$rxJava2Android" compile "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
with the versions
ext { .... retrofitVersion = '2.3.0' okHttpVersion = '3.9.0' gsonVersion = '2.8.2' rxJava2 = '2.1.5' rxJava2Android = '2.0.1' .... }
Note: update the versions to the last version of the libraries
After building the App, the library are available and ready to be used.
In the ‘service’ package create the package ‘api’, and in the ‘service.api’ package create the interface NetworkingAPI.java
public interface NetworkingAPI { // Define the endpoints of the Restful service }
and here you can define the endpoints required.
In the NetworkingModule.java class, add the private methods
private OkHttpClient getOkHttpClient() { HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(); httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); return new OkHttpClient .Builder() .addInterceptor(httpLoggingInterceptor).build(); } private Retrofit getRetrofit(String baseUrl) { return new Retrofit .Builder() .baseUrl(baseUrl) .client(getOkHttpClient()) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // Link between Retrofit 2 and RxJava 2 .build(); }
and in the same class update the getNetworkingService method:
@Provides @AppScope public NetworkingService getNetworkingService(Context context, ErrorService errorService) { NetworkingAPI networkingAPI = getRetrofit("http://www.serveradddress.com").create(NetworkingAPI.class); return new NetworkingService(context, errorService, networkingAPI); }
You will not be able to compile the project because we are invoking the NetworkingService constructor with the additional parameter ‘networkingAPI’, so we have to update the constructor of NetworkingService.java as follow
public NetworkingService(Context context, ErrorService errorService, NetworkingAPI networkingAPI) { this.context = context; this.errorService = errorService; this.networkingAPI = networkingAPI; }
Now you can remove the login/logout methods in the NetworkingService class and implement the methods required using the methods defined in the NetworkAPI interface.
Here is now the status of our project:
MVVM architecture (Model – View – ViewModel)Dagger 2DataBindingRetrofitRxJava
and this mean that the mvvm structure of the App is fully implemented and now you can continue to implement the rest of your App.
MVVM Templates on GitHub
Here there are the templates for Android Studio 2.3.3 and 3.1.1:
Android Studio 2.3.3: GitHub
Android Studio 3.1.1: GitHub
I love your Android MVVM App tutorial.
Would be nice if you make it a video =)
Thanks!
LikeLike
Thank you !
I have already planned a serie of videos, the problem is finding the time to dedicate to them. Hopefully they will be available soon.
LikeLike
Can I get your source code for this tutorial ?
Thanks
LikeLike
Dear DIEGONOVATI,
I had copied all of your code in new project, and it worked without any bugs.
However, I am stuck that like how can I continue the rest ?
Few queries:
1) I know to insert my login API Endpoint in the NetworkingAPI interface, next should I do the same as usual for creating Login method in NetworkingService ?
2) When should I edit the module or add my own module in what kind of cases ?
Sorry for my questions if they make you unclear or unrelated to this tutorial.
Thank you once again. Appreaciated =)
LikeLike
Dear DIEGONOVATI,
I had copied all of your code in new project, and it worked without any bugs.
However, I am stuck that like how can I continue the rest ?
Few queries:
1) I know to insert my login API Endpoint in the NetworkingAPI interface, next should I do the same as usual for creating Login method in NetworkingService ?
2) When should I edit the module or add my own module in what kind of cases ?
Sorry for my questions if they make you unclear or unrelated to this tutorial.
Thank you once again. Appreciated. =)
LikeLike
1) I know to insert my login API Endpoint in the NetworkingAPI interface, next should I do the same as usual for creating Login method in NetworkingService ?
If the login endpoint has the same root url of the others endpoint you have to add it in the Networking API, otherwise you have to create a new Service (I my case in one of my projects I created the AuthenticationService)
2) When should I edit the module or add my own module in what kind of cases ?
You are free to add any module you need, there are no restrictions. In all my MVVM projects for example I create the AndroidService that contains the procedures to open new activities and close the current one: in this way I can mock them easily during the testing of the viewmodels.
LikeLike
At the end of the article I have added the links to the templates available on GitHub
LikeLike
can you share your this project mvvm on github? thank you in advance 😀
LikeLike
At the end of the article I have added the links to the templates available on GitHub
LikeLike
Hello Mr.Diegonvati I did the expalined steps but how we can add the service repositry to our app
LikeLike
The services are injected using Dagger as you can see in the RootActivity:
@Inject
protected NetworkingService networkingService;
LikeLike
Hello Mr.Diegonovati , hope you are well ,kindly I need your helps how to do login and registration. if you have any sample provide me to folow it
LikeLike
could you add more explaintion for login and registration methods
using your project structure
LikeLike