7 Steps To Implement Dagger 2 in Android by Anit
7 Steps To Implement Dagger 2 in Android by Anit
Search Write
3.8K 16
3.8K 16
Image Credits: https://unsplash.com/photos/crs2vlkSe98
So I finally got a chance to explore the new Dagger 2 in a project recently. And while
there are a lot of resources online about what is Dagger and why dagger 2 is necessary
and how to implement Dagger 2, I found that there was a steep learning curve to it.
While I understood why dagger 2 is not only necessary but a must in some cases, I
found practical implementation difficult. So I thought I would write about the 7 basic
steps to implement Dagger 2 in an Android app.
Note: This article focuses on implementation of dagger using the new dagger android
injection.
I chose to build a simple movies app to test the dagger implementation. This is the
app.
This app displays a list of movies or tv shows from the famous TMDB api. There is a
details page which displays the movie or tv show videos, reviews, cast and crew info
etc. We are going to build a simplified version of this app. i.e. we are going to focus on
building a single screen app that fetches a list of movies from the TMDB api and
displays it in the app.
For those of you interested in skipping the article, the GitHub link is for this
implementation is here.
So let’s begin!
Java:
1 apply plugin: 'com.android.application'
2
3 android {
4 compileSdkVersion 28
5 defaultConfig {
6 applicationId "com.an.dagger"
7 minSdkVersion 16
8 targetSdkVersion 28
9 versionCode 1
10 versionName "1.0"
11 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
12 }
13 buildTypes {
14 release {
15 minifyEnabled false
16 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules
17 }
18 }
19
20 /* I use Data Binding and Java8 in all my projects */
21 dataBinding {
22 enabled true
23 }
24 compileOptions {
25 sourceCompatibility JavaVersion.VERSION_1_8
26 targetCompatibility JavaVersion.VERSION_1_8
27 }
28 }
29
30 dependencies {
31 implementation fileTree(dir: 'libs', include: ['*.jar'])
32 testImplementation 'junit:junit:4.12'
33 androidTestImplementation 'com.android.support.test:runner:1.0.2'
34 androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
35
36 /* Android Support Library: RecyclerView, CardView */
37 implementation 'com.android.support:appcompat-v7:28.0.0'
38 implementation 'com.android.support:recyclerview-v7:28.0.0'
39 implementation 'com.android.support:design:28.0.0'
40 implementation 'com.android.support:cardview-v7:28.0.0'
41
42 /* Retrofit using RxJava2, Okhttp, Okhttp logging interceptor, Gson */
43 implementation 'com.squareup.retrofit2:retrofit:2.4.0'
44 implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
45 implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
46 implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
47
48
49 /* Picasso lib for image loading */
50 implementation 'com.squareup.picasso:picasso:2.71828'
51
52 /* Android Architecture Component - ConstraintLayout */
53 implementation 'com.android.support.constraint:constraint-layout:1.0.2'
54
55 /* Android Architecture Component - LiveData & ViewModel */
56 implementation 'android.arch.lifecycle:extensions:1.1.1'
57
58 /* Android Architecture Component - Room Persistance Lib */
59 implementation 'android.arch.persistence.room:runtime:1.1.1'
60 implementation 'android.arch.persistence.room:rxjava2:1.1.1'
61 annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
62
63 /* Dagger2 - We are going to use dagger.android which includes
64 * support for Activity and fragment injection so we need to include
65 * the following dependencies */
66 implementation 'com.google.dagger:dagger-android:2.17'
67 implementation 'com.google.dagger:dagger-android-support:2.17'
68 annotationProcessor 'com.google.dagger:dagger-android-processor:2.17'
69
70 /* Dagger2 - default dependency */
71 annotationProcessor 'com.google.dagger:dagger-compiler:2.17'
72 }
1. Creating an Entity:
MovieEntity.java:
1 @Entity(primaryKeys = ("id"))
2 public class MovieEntity implements Parcelable {
3
4 @SerializedName("id")
5 @Expose
6 private Long id;
7
8 @SerializedName(value="header", alternate={"title", "name"})
9 @Expose
10 private String header;
11
12 @SerializedName("poster_path")
13 @Expose
14 private String posterPath;
15
16 @SerializedName(value="description", alternate={"overview", "synopsis"})
17 private String description;
18
19 @SerializedName("release_date")
20 @Expose
21 private String releaseDate;
22
23 @SerializedName("runtime")
24 @Expose
25 private Long runtime;
26
27 @SerializedName("status")
28 @Expose
29 private String status;
30
31 public Long getId() {
32 return id;
33 }
34
35 public void setId(Long id) {
36 this.id = id;
37 }
38
39 public String getHeader() {
40 return header;
41 }
42
43 public void setHeader(String header) {
44 this.header = header;
45 }
46
47 public String getPosterPath() {
48 return posterPath;
49 }
50
51 public void setPosterPath(String posterPath) {
52 this.posterPath = posterPath;
53 }
54
55 public String getDescription() {
56 return description;
57 }
58
59 public void setDescription(String description) {
60 this.description = description;
61 }
62
63 public String getReleaseDate() {
64 return releaseDate;
65 }
66
67 public void setReleaseDate(String releaseDate) {
68 this.releaseDate = releaseDate;
69 }
70
71 public Long getRuntime() {
72 return runtime;
73 }
74
75 public void setRuntime(Long runtime) {
76 this.runtime = runtime;
77 }
78
79 public String getStatus() {
80 return status;
81 }
82
83 public void setStatus(String status) {
84 this.status = status;
85 }
86
87
88 @Override
89 public int describeContents() {
90 return 0;
91 }
92
93 @Override
94 public void writeToParcel(Parcel dest, int flags) {
95 dest.writeValue(this.id);
96 dest.writeString(this.header);
97 dest.writeString(this.posterPath);
98 dest.writeString(this.description);
99 dest.writeString(this.releaseDate);
100 dest.writeValue(this.runtime);
101 dest.writeString(this.status);
102 }
103
104 public MovieEntity() {
105 }
106
107 protected MovieEntity(Parcel in) {
108 this.id = (Long) in.readValue(Long.class.getClassLoader());
109 this.header = in.readString();
110 this.posterPath = in.readString();
111 this.description = in.readString();
112 this.releaseDate = in.readString();
113 this.runtime = (Long) in.readValue(Long.class.getClassLoader());
114 this.status = in.readString();
115 }
116
117 public static final Creator<MovieEntity> CREATOR = new Creator<MovieEntity>() {
118 @Override
119 public MovieEntity createFromParcel(Parcel source) {
120 return new MovieEntity(source);
121 }
122
123 @Override
124 public MovieEntity[] newArray(int size) {
125 return new MovieEntity[size];
126 }
127 };
128 }
MovieDao.java:
1 @Dao
2 public interface MovieDao {
3
4 @Insert(onConflict = OnConflictStrategy.REPLACE)
5 long[] insertMovies(List<MovieEntity> movies);
6
7 @Query("SELECT * FROM `MovieEntity`")
8 List<MovieEntity> getMoviesByPage();
9 }
MovieDao.kt:
1 @Dao
2 interface MovieDao {
3
4 /* Method to insert the movies fetched from api
5 * to room */
6 @Insert(onConflict = OnConflictStrategy.REPLACE)
7 fun insertMovies(movies: List<MovieEntity>): LongArray
8
9 /* Method to fetch the movies stored locally */
10 @Query("SELECT * FROM `MovieEntity`")
11 fun getMoviesByPage(): List<MovieEntity>
12 }
AppDatabase.java:
AppDatabase.kt:
1 @Database(entities = [MovieEntity::class], version = 1, exportSchema = false)
2 abstract class AppDatabase : RoomDatabase() {
3
4 abstract fun movieDao(): MovieDao
5 }
Note: If you would like to know more about Room persistence Library and how to configure
it, then checkout this article
MovieApiResponse.java:
1 public class MovieApiResponse {
2
3 public MovieApiResponse() {
4 this.results = new ArrayList<>();
5 }
6
7 private long page;
8
9 @SerializedName("total_pages")
10 private long totalPages;
11
12 @SerializedName("total_results")
13 private long totalResults;
14
15 private List<MovieEntity> results;
16
17 public long getPage() {
18 return page;
19 }
20
21 public void setPage(long page) {
22 this.page = page;
23 }
24
25 public long getTotalPages() {
26 return totalPages;
27 }
28
29 public void setTotalPages(long totalPages) {
30 this.totalPages = totalPages;
31 }
32
33 public long getTotalResults() {
34 return totalResults;
35 }
36
37 public void setTotalResults(long totalResults) {
38 this.totalResults = totalResults;
39 }
40
41 public List<MovieEntity> getResults() {
42 return results;
43 }
44
45 public void setResults(List<MovieEntity> results) {
46 this.results = results;
47 }
48 }
MovieApiService.kt:
1 interface MovieApiService {
2
3 @GET("movie/popular?language=en-US®ion=US&page=1")
4 fun fetchMoviesByType(): Observable<MovieApiResponse>
5 }
MovieRepository.java:
1 /*
2 * One of the first things we do in the Repository class
3 * is to make it a Singleton.
4 * */
5
6 @Singleton
7 public class MovieRepository {
8
9 private MovieDao movieDao;
10 private MovieApiService movieApiService;
11 public MovieRepository(MovieDao movieDao,
12 MovieApiService movieApiService) {
13 this.movieDao = movieDao;
14 this.movieApiService = movieApiService;
15 }
16
17
18
19 /*
20 * We are using this method to fetch the movies list
21 * NetworkBoundResource is part of the Android architecture
22 * components. You will notice that this is a modified version of
23 * that class. That class is based on LiveData but here we are
24 * using Observable from RxJava.
25 *
26 * There are three methods called:
27 * a. fetch data from server
28 * b. fetch data from local
29 * c. save data from api in local
30 *
31 * So basically we fetch data from server, store it locally
32 * and then fetch data from local and update the UI with
33 * this data.
34 *
35 * */
36 public Observable<Resource<List<MovieEntity>>> loadMoviesByType() {
37 return new NetworkBoundResource<List<MovieEntity>, MovieApiResponse>() {
38
39 @Override
40 protected void saveCallResult(@NonNull MovieApiResponse item) {
41 movieDao.insertMovies(item.getResults());
42 }
43
44 @Override
45 protected boolean shouldFetch() {
46 return true;
47 }
48
49 @NonNull
50 @Override
51 protected Flowable<List<MovieEntity>> loadFromDb() {
52 List<MovieEntity> movieEntities = movieDao.getMoviesByPage();
53 if(movieEntities == null || movieEntities.isEmpty()) {
54 return Flowable.empty();
55 }
56 return Flowable.just(movieEntities);
57 }
58
59 @NonNull
60 @Override
61 protected Observable<Resource<MovieApiResponse>> createCall() {
62 return movieApiService.fetchMovies()
63 .flatMap(movieApiResponse -> Observable.just(movieApiResponse == null
64 ? Resource.error("", new MovieApiResponse())
65 : Resource.success(movieApiResponse)));
66 }
67 }.getAsObservable();
68 }
69 }
The repository class is responsible for fetching the data (either from web api or
local cache).
The ViewModel is responsible for updating the UI with the data changes. The
ViewModel will initialise an instance of the Repository class and update the UI
based with this data.
For this, the ViewModel must have access to the using MovieDao class and the
MovieApiService class. This is where Dagger comes in. We will be injecting the
ViewModel with the MovieDao class and the MovieApiService class.
MovieListViewModel.java:
1 public class MovieListViewModel extends ViewModel {
2
3 private MovieRepository movieRepository;
4
5 /* We are using LiveData to update the UI with the data changes.
6 */
7 private MutableLiveData<Resource<List<MovieEntity>>> moviesLiveData = new MutableLiveData<>();
8
9
10 /*
11 * We are injecting the MovieDao class
12 * and the MovieApiService class to the ViewModel.
13 * */
14
15 @Inject
16 public MovieListViewModel(MovieDao movieDao, MovieApiService movieApiService) {
17 /* You can see we are initialising the MovieRepository class here */
18 movieRepository = new MovieRepository(movieDao, movieApiService);
19 }
20
21
22 /*
23 * Method called by UI to fetch movies list
24 * */
25 public void loadMoreMovies() {
26 movieRepository.loadMoviesByType()
27 .subscribe(resource -> getMoviesLiveData().postValue(resource));
28 }
29
30
31 /*
32 * LiveData observed by the UI
33 * */
34 public MutableLiveData<Resource<List<MovieEntity>>> getMoviesLiveData() {
35 return moviesLiveData;
36 }
37 }
In Dagger, we can annotate classes with @Module . These classes are responsible for
providing objects/classes which can be injected. Such classes can define methods
annotated with @ Provides . The returned objects from these methods are available
for dependency injection.
So in our case, we need to inject two classes: MovieDao class and MovieApiService
class. So we are going to create two Modules: ApiModule and DbModule (This can be
done in a single module but I prefer to keep local and web service separate).
1. ApiModule class
ApiModule.java
1 @Module
2 public class ApiModule {
3
4 /*
5 * The method returns the Gson object
6 * */
7 @Provides
8 @Singleton
9 Gson provideGson() {
10 GsonBuilder gsonBuilder = new GsonBuilder();
11 return gsonBuilder.create();
12 }
13
14
15 /*
16 * The method returns the Cache object
17 * */
18 @Provides
19 @Singleton
20 Cache provideCache(Application application) {
21 long cacheSize = 10 * 1024 * 1024; // 10 MB
22 File httpCacheDirectory = new File(application.getCacheDir(), "http-cache");
23 return new Cache(httpCacheDirectory, cacheSize);
24 }
25
26
27 /*
28 * The method returns the Okhttp object
29 * */
30 @Provides
31 @Singleton
32 OkHttpClient provideOkhttpClient(Cache cache) {
33 HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
34 logging.setLevel(HttpLoggingInterceptor.Level.BODY);
35
36 OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
37 httpClient.cache(cache);
38 httpClient.addInterceptor(logging);
39 httpClient.addNetworkInterceptor(new RequestInterceptor());
40 httpClient.connectTimeout(30, TimeUnit.SECONDS);
41 httpClient.readTimeout(30, TimeUnit.SECONDS);
42 return httpClient.build();
43 }
44
45
46 /*
47 * The method returns the Retrofit object
48 * */
49 @Provides
50 @Singleton
51 Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) {
52 return new Retrofit.Builder()
53 .addConverterFactory(GsonConverterFactory.create(gson))
54 .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
55 .baseUrl("https://api.themoviedb.org/3/")
56 .client(okHttpClient)
57 .build();
58 }
59
60 /*
61 * We need the MovieApiService module.
62 * For this, We need the Retrofit object, Gson, Cache and OkHttpClient .
63 * So we will define the providers for these objects here in this module.
64 *
65 * */
66
67 @Provides
68 @Singleton
69 MovieApiService provideMovieApiService(Retrofit retrofit) {
70 return retrofit.create(MovieApiService.class);
71 }
72 }
DbModule.java
1 @Module
2 public class DbModule {
3
4 /*
5 * The method returns the Database object
6 * */
7 @Provides
8 @Singleton
9 AppDatabase provideDatabase(@NonNull Application application) {
10 return Room.databaseBuilder(application,
11 AppDatabase.class, "Entertainment.db")
12 .allowMainThreadQueries().build();
13 }
14
15
16 /*
17 * We need the MovieDao module.
18 * For this, We need the AppDatabase object
19 * So we will define the providers for this here in this module.
20 * */
21
22 @Provides
23 @Singleton
24 MovieDao provideMovieDao(@NonNull AppDatabase appDatabase) {
25 return appDatabase.movieDao();
26 }
27 }
1 @Module
2 class DbModule {
3
4 /*
5 * The method returns the Database object
6 * */
7 @Provides
8 @Singleton
9 internal fun provideDatabase(application: Application): AppDatabase {
10 return Room.databaseBuilder(
11 application, AppDatabase::class.java, "Entertainment.db")
12 .allowMainThreadQueries().build()
13 }
14
15
16 /*
17 * We need the MovieDao module.
18 * For this, We need the AppDatabase object
19 * So we will define the providers for this here in this module.
20 * */
21 @Provides
22 @Singleton
23 internal fun provideMovieDao(appDatabase: AppDatabase): MovieDao {
24 return appDatabase.movieDao()
25 }
26 }
ViewModelFactory.java
1 @Singleton
2 public class ViewModelFactory implements ViewModelProvider.Factory {
3
4 private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
5
6 @Inject
7 public ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
8 this.creators = creators;
9 }
10
11 @Override
12 public <T extends ViewModel> T create(Class<T> modelClass) {
13 Provider<? extends ViewModel> creator = creators.get(modelClass);
14 if (creator == null) {
15 for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entryS
16 if (modelClass.isAssignableFrom(entry.getKey())) {
17 creator = entry.getValue();
18 break;
19 }
20 }
21 }
22 if (creator == null) {
23 throw new IllegalArgumentException("unknown model class " + modelClass);
24 }
25 try {
26 return (T) creator.get();
27 } catch (Exception e) {
28 throw new RuntimeException(e);
29 }
30 }
31 }
1 @Singleton
2 class ViewModelFactory @Inject
3 constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewMo
4
5 override fun <T : ViewModel> create(modelClass: Class<T>): T {
6 val creator = viewModels[modelClass]
7 ?: viewModels.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
8 ?: throw IllegalArgumentException("unknown model class $modelClass")
9 return try {
10 creator.get() as T
11 } catch (e: Exception) {
12 throw RuntimeException(e)
13 }
14 }
15 }
4. ViewModelKey class:
ViewModelKeys helps you map your ViewModel classes so ViewModelFactory can
correctly provide/inject them.
ViewModelKey.java
1 @Target({ElementType.METHOD})
2 @Retention(RetentionPolicy.RUNTIME)
3 @MapKey
4 public @interface ViewModelKey {
5 Class<? extends ViewModel> value();
6 }
ViewModelKey.kt
1 @Target(AnnotationTarget.FUNCTION,
2 AnnotationTarget.PROPERTY_GETTER,
3 AnnotationTarget.PROPERTY_SETTER)
4 @MapKey
5 annotation class ViewModelKey(val value: KClass<out ViewModel>)
4. ViewModelModule class
The ViewModelModule is used to provide a map of view models through dagger that is
used by the ViewModelFactory class.
ViewModelModule.java
1 @Module
2 public abstract class ViewModelModule {
3
4 @Binds
5 abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory factory);
6
7
8 /*
9 * This method basically says
10 * inject this object into a Map using the @IntoMap annotation,
11 * with the MovieListViewModel.class as key,
12 * and a Provider that will build a MovieListViewModel
13 * object.
14 *
15 * */
16
17 @Binds
18 @IntoMap
19 @ViewModelKey(MovieListViewModel.class)
20 protected abstract ViewModel movieListViewModel(MovieListViewModel moviesListViewModel);
21 }
ViewModelModule.kt
1 @Module
2 internal abstract class ViewModelModule {
3
4 @Binds
5 internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Facto
6
7
8 /*
9 * This method basically says
10 * inject this object into a Map using the @IntoMap annotation,
11 * with the MovieListViewModel.class as key,
12 * and a Provider that will build a MovieListViewModel
13 * object.
14 *
15 * */
16
17 @Binds
18 @IntoMap
19 @ViewModelKey(MovieListViewModel::class)
20 protected abstract fun movieListViewModel(moviesListViewModel: MovieListViewModel): ViewModel
21 }
So basically,
5. ActivityModule class
Since we are using the dagger-android support library, we can make use of Android
Injection. The ActivityModule generates AndroidInjector (this is the new dagger-
android class which exist in dagger-android framework) for Activities defined in this
class. This allows us to inject things into Activities using
AndroidInjection.inject(this) in the onCreate() method.
ActivityModule.java
1 @Module
2 public abstract class ActivityModule {
3
4 @ContributesAndroidInjector()
5 abstract MainActivity contributeMainActivity();
6 }
ActivityModule.kt
1 @Module
2 abstract class ActivityModule {
3
4 @ContributesAndroidInjector()
5 abstract fun contributeMainActivity(): MainActivity
6 }
6. AppComponent class
Any class with the annotation @Component defines the connection between the
modules and the classes which need the dependency. We define a @Component.Builder
interface which will be called from our custom Application class. This will set our
application object to the AppComponent . So inside the AppComponent the application
instance is available. So this application instance can be accessed by our modules
such as ApiModule when needed.
AppComponent.java
1 /*
2 * We mark this interface with the @Component annotation.
3 * And we define all the modules that can be injected.
4 * Note that we provide AndroidSupportInjectionModule.class
5 * here. This class was not created by us.
6 * It is an internal class in Dagger 2.10.
7 * Provides our activities and fragments with given module.
8 * */
9 @Component(modules = {
10 ApiModule.class,
11 DbModule.class,
12 ViewModelModule.class,
13 ActivityModule.class,
14 AndroidSupportInjectionModule.class})
15 @Singleton
16 public interface AppComponent {
17
18
19 /* We will call this builder interface from our custom Application class.
20 * This will set our application object to the AppComponent.
21 * So inside the AppComponent the application instance is available.
22 * So this application instance can be accessed by our modules
23 * such as ApiModule when needed
24 * */
25 @Component.Builder
26 interface Builder {
27
28 @BindsInstance
29 Builder application(Application application);
30
31 AppComponent build();
32 }
33
34
35 /*
36 * This is our custom Application class
37 * */
38 void inject(AppController appController);
39 }
AppController.java
1 /*
2 * we use our AppComponent (now prefixed with Dagger)
3 * to inject our Application class.
4 * This way a DispatchingAndroidInjector is injected which is
5 * then returned when an injector for an activity is requested.
6 * */
7 public class AppController extends Application implements HasActivityInjector {
8
9 @Inject
10 DispatchingAndroidInjector<Activity> dispatchingAndroidInjector;
11
12 @Override
13 public DispatchingAndroidInjector<Activity> activityInjector() {
14 return dispatchingAndroidInjector;
15 }
16
17 @Override
18 public void onCreate() {
19 super.onCreate();
20 DaggerAppComponent.builder()
21 .application(this)
22 .build()
23 .inject(this);
24 }
25 }
AppController.kt
1 /*
2 * we use our AppComponent (now prefixed with Dagger)
3 * to inject our Application class.
4 * This way a DispatchingAndroidInjector is injected which is
5 * then returned when an injector for an activity is requested.
6 * */
7 class AppController : Application(), HasActivityInjector {
8
9 @Inject
10 lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
11
12 override fun activityInjector(): DispatchingAndroidInjector<Activity>? {
13 return dispatchingAndroidInjector
14 }
15
16 override fun onCreate() {
17 super.onCreate()
18 DaggerAppComponent.builder()
19 .application(this)
20 .build()
21 .inject(this)
22 }
23 }
Note: Don’t forget to add this custom Application class to the AndroidManifest.xml
<application
android:name=".AppController"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"/>
MainActivity.java
1 public class MainActivity extends AppCompatActivity {
2
3 /*
4 * Step 1: Here as mentioned in Step 5, we need to
5 * inject the ViewModelFactory. The ViewModelFactory class
6 * has a list of ViewModels and will provide
7 * the corresponding ViewModel in this activity
8 * */
9 @Inject
10 ViewModelFactory viewModelFactory;
11
12 /*
13 * I am using DataBinding
14 * */
15 private MainActivityBinding binding;
16
17 /*
18 * This is our ViewModel class
19 * */
20 private MovieListViewModel movieListViewModel;
21
22 private MoviesListAdapter moviesListAdapter;
23
24 @Override
25 protected void onCreate(@Nullable Bundle savedInstanceState) {
26 /*
27 * Step 2: Remember in our ActivityModule, we
28 * defined MainActivity injection? So we need
29 * to call this method in order to inject the
30 * ViewModelFactory into our Activity
31 * */
32 AndroidInjection.inject(this);
33
34 super.onCreate(savedInstanceState);
35 initialiseView();
36 initialiseViewModel();
37 }
38
39 /*
40 * Initialising the View using Data Binding
41 * */
42 private void initialiseView() {
43 binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
44
45 moviesListAdapter = new MoviesListAdapter(this);
46 binding.moviesList.setLayoutManager(new LinearLayoutManager(getApplicationContext(), Linea
47 binding.moviesList.setAdapter(moviesListAdapter);
48
49 /* SnapHelper to change the background of the activity based on the list item
50 * currently visible */
51 SnapHelper startSnapHelper = new PagerSnapHelper(position -> {
52 MovieEntity movie = moviesListAdapter.getItem(position);
53 binding.overlayLayout.updateCurrentBackground(movie.getPosterPath());
54 });
55 startSnapHelper.attachToRecyclerView(binding.moviesList);
56 }
57
58
59 /*
60 * Step 3: Initialising the ViewModel class here.
61 * We are adding the ViewModelFactory class here.
62 * We are observing the LiveData
63 * */
64 private void initialiseViewModel() {
65 movieListViewModel = ViewModelProviders.of(this, viewModelFactory).get(MovieListViewModel
66 movieListViewModel.getMoviesLiveData().observe(this, resource -> {
67 if(resource.isLoading()) {
68 displayLoader();
69
70 } else if(!resource.data.isEmpty()) {
71 updateMoviesList(resource.data);
72
73 } else handleErrorResponse();
74 });
75
76 /* Fetch movies list */
77 movieListViewModel.loadMoreMovies();
78 }
79
80 private void displayLoader() {
81 binding.moviesList.setVisibility(View.GONE);
82 binding.loaderLayout.rootView.setVisibility(View.VISIBLE);
83 binding.loaderLayout.loader.start();
84 }
85
86 private void hideLoader() {
87 binding.moviesList.setVisibility(View.VISIBLE);
88 binding.loaderLayout.rootView.setVisibility(View.GONE);
89 binding.loaderLayout.loader.stop();
90 }
91
92 private void updateMoviesList(List<MovieEntity> movies) {
93 hideLoader();
94 binding.emptyLayout.emptyContainer.setVisibility(View.GONE);
95 binding.moviesList.setVisibility(View.VISIBLE);
96 moviesListAdapter.setItems(movies);
97 }
98
99
100 private void handleErrorResponse() {
101 hideLoader();
102 binding.moviesList.setVisibility(View.GONE);
103 binding.emptyLayout.emptyContainer.setVisibility(View.VISIBLE);
104 }
105 }
And that’s it! We build and run our app and this is the output we should receive.
Note: If you would like to know how to implement the BackgroundSwitcherView i.e. change
the background of the activity when the recyclerView is scrolled, checkout this link.
Bonus:
We saw how to inject the ViewModelFactory into our MainActivity . If you would like to
inject the ViewModelFactory into a Fragment, we need to do the following
modifications:
1. FragmentModule class:
Create a new class called FragmentModule .
FragmentModule.java
1 @Module
2 public abstract class FragmentModule {
3 /*
4 * We define the name of the Fragment we are going
5 * to inject the ViewModelFactory into. i.e. in our case
6 * The name of the fragment: MovieListFragment
7 */
8 @ContributesAndroidInjector
9 abstract MovieListFragment contributeMovieListFragment();
10 }
FragmentModule.kt
1 @Module
2 abstract class FragmentModule {
3 /*
4 * We define the name of the Fragment we are going
5 * to inject the ViewModelFactory into. i.e. in our case
6 * The name of the fragment: MovieListFragment
7 */
8 @ContributesAndroidInjector
9 abstract fun contributeMovieListFragment(): MovieListFragment
10 }
1 @Module
2 public abstract class ActivityModule {
3
4 /*
5 * We modify our ActivityModule by adding the
6 * FragmentModule to the Activity which contains
7 * the fragment
8 */
9 @ContributesAndroidInjector(modules = FragmentModule.class)
10 abstract MainActivity contributeMainActivity();
11 }
ActivityModule.kt
1 @Module
2 abstract class ActivityModule {
3 /*
4 * We modify our ActivityModule by adding the
5 * FragmentModule to the Activity which contains
6 * the fragment
7 */
8 @ContributesAndroidInjector(modules = [FragmentModule::class])
9 abstract fun contributeMainActivity(): MainActivity
10 }
MainActivity.java
1 public class MainActivity extends AppCompatActivity implements HasSupportFragmentInjector {
2
3 /*
4 * Step 1: Rather than injecting the ViewModelFactory
5 * in the activity, we are going to implement the
6 * HasActivityInjector and inject the ViewModelFactory
7 * into our MovieListFragment
8 * */
9 @Inject
10 DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
11
12 @Override
13 public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
14 return dispatchingAndroidInjector;
15 }
16
17 /*
18 * I am using DataBinding
19 * */
20 private MainActivityBinding binding;
21
22
23 @Override
24 protected void onCreate(@Nullable Bundle savedInstanceState) {
25 /*
26 * Step 2: We still need to inject this method
27 * into our activity so that our fragment can
28 * inject the ViewModelFactory
29 * */
30 AndroidInjection.inject(this);
31
32 super.onCreate(savedInstanceState);
33 initialiseView();
34 }
35
36 /*
37 * Initialising the View using Data Binding
38 * */
39 private void initialiseView() {
40 binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
41 }
42
43 public void updateBackground(String url) {
44 binding.overlayLayout.updateCurrentBackground(url);
45 }
46 }
MovieListFragment.java
1 public class MovieListFragment extends Fragment {
2
3 /*
4 * Step 1: Here, we need to inject the ViewModelFactory.
5 * */
6 @Inject
7 ViewModelFactory viewModelFactory;
8
9 /*
10 * I am using DataBinding
11 * */
12 private MovieFragmentBinding binding;
13
14 /*
15 * This is our ViewModel class
16 * */
17 MovieListViewModel movieListViewModel;
18
19 private MoviesListAdapter moviesListAdapter;
20
21 @Override
22 public void onCreate(@Nullable Bundle savedInstanceState) {
23 super.onCreate(savedInstanceState);
24 /*
25 * Step 2: Remember in our FragmentModule, we
26 * defined MovieListFragment injection? So we need
27 * to call this method in order to inject the
28 * ViewModelFactory into our Fragment
29 * */
30 AndroidSupportInjection.inject(this);
31 initialiseViewModel();
32 }
33
34 @Nullable
35 @Override
36 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nu
37 binding = DataBindingUtil.inflate(inflater, R.layout.fragment_movie_list, container, false
38 return binding.getRoot();
39 }
40
41 @Override
42 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
43 super.onViewCreated(view, savedInstanceState);
44 initialiseView();
45 }
46
47 private void initialiseView() {
48 moviesListAdapter = new MoviesListAdapter(getActivity());
49 binding.moviesList.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayout
50 binding.moviesList.setAdapter(moviesListAdapter);
51
52 /* SnapHelper to change the background of the activity based on the list item
53 * currently visible */
54 SnapHelper startSnapHelper = new PagerSnapHelper(position -> {
55 MovieEntity movie = moviesListAdapter.getItem(position);
56 ((MainActivity)getActivity()).updateBackground(movie.getPosterPath());
57 });
58 startSnapHelper.attachToRecyclerView(binding.moviesList);
59 }
60
61
62 private void initialiseViewModel() {
63 movieListViewModel = ViewModelProviders.of(this, viewModelFactory).get(MovieListViewModel
64 movieListViewModel.getMoviesLiveData().observe(this, resource -> {
65 if(resource.isLoading()) {
66 displayLoader();
67
68 } else if(!resource.data.isEmpty()) {
69 updateMoviesList(resource.data);
70
71 } else handleErrorResponse();
72 });
73
74 /* Fetch movies list */
75 movieListViewModel.loadMoreMovies();
76 }
77
78 private void displayLoader() {
79 binding.moviesList.setVisibility(View.GONE);
80 binding.loaderLayout.rootView.setVisibility(View.VISIBLE);
81 }
82
83 private void hideLoader() {
84 binding.moviesList.setVisibility(View.VISIBLE);
85 binding.loaderLayout.rootView.setVisibility(View.GONE);
86 }
87
88 private void updateMoviesList(List<MovieEntity> movies) {
89 hideLoader();
90 binding.emptyLayout.emptyContainer.setVisibility(View.GONE);
91 binding.moviesList.setVisibility(View.VISIBLE);
92 moviesListAdapter.setItems(movies);
93 }
94
95
96 private void handleErrorResponse() {
97 hideLoader();
98 binding.moviesList.setVisibility(View.GONE);
99 binding.emptyLayout.emptyContainer.setVisibility(View.VISIBLE);
100 }
101 }
@Component(
modules = [
ApiModule::class,
DbModule::class,
ViewModelModule::class,
ActivityModule::class,
FragmentModule::class,
AndroidSupportInjectionModule::class]
)
You can checkout this commit to find out the differences between injecting into
Activity and injecting into fragment for Java and this commit for Kotlin.
And that’s it! 😄. I know it feels overwhelming and kinda a lot of work but in the end
it’s worth it, I think.
You can find the GitHub link for this sample implementation here:
For those of you interested in the whole app implementation, (and not just the
simplified version we implemented here), you can checkout the Github project from
here.
I hope you enjoyed this article and found it useful, if so please hit the Clap button. Let
me know your thoughts in the comments section.
Android Development
Written by Anitaa Murthy Follow
15.4K 71 949 8
Ankit Sinhal in AndroidPub Anitaa Murthy in AndroidPub
5 min read · Jan 14, 2017 6 min read · May 16, 2018
3.2K 17 3.6K 22
32 1 10
Lists
Understanding Eventbus with kotlin Building Android Apps with MVVM and
Flow Clean Architecture
The EventBus pattern is a popular design pattern In the world of Android development, designing a
for communication between different parts of an… robust and maintainable architecture is crucial.…
44 4 70
78 442 4