Explore How Dagger 2 Enables the Reusability of the Components?
When a group of developers at Square found Guice to be too slow as the Java dependency injection, they started exploring the possibilities with annotation-based code generation approach and introduced Dagger in 2012. With the awaited release of Dagger 2, it has been established as a mature dependency injection and developers around the world love to avail its advantages.
What is Dagger 2?
Based on the Java Specification Request (JSR) 330, Dagger 2 is the dependency injection framework for Java and Android. Being an annotation-based, it does not rely upon XML and offers easier code decoupling, abstraction, testability and maintainability for high performing applications.
Teamed up with its original developers at Square, Dagger 2 is developed by Google. It uses generated code to access the fields, not the reflection, and empowers developers to write cleaner code.
In this blog, I have detailed out Dagger 2's strengths along with an example of implementing Dagger 2 for a real world application in combination of Retrofit library and MVP (Model View Presenter) in the quickest possible manner, to my knowledge.
A Brief Introduction to Dagger 2 Annotations
A few of the Dagger 2 annotations are explained below.
@Inject annotation
@Inject is used to request desired dependencies. You need to annotate classes here so that Dagger can build the relevant instances, e.g., @Inject.
public ClassName classname;
@Module annotation
@Modules are the classes and their methods will deliver the desired dependencies. Hence, once you define the class, you need to annotate it with @Module. As a result, Dagger will realize that where to find the dependencies. It is possible to have multiple composed modules.
E.g.
@Module
public class NetworkModule {
}
@Provide annotation
@Provide will help Dagger in defining your desired the dependencies.
E.g.
@Provides
public void methodName() {
}
@Component annotation
@Components fundamentally are the injectors. They create connection between @Inject and @Module. Therefore, it is necessary to annotate an interface with @Component as well as list out all the @Modules that are used in formulating particular that component.
Any type of mistake in annotating @Inject listing out @Modules can result as major errors at the time of compilation. In other words, we can say that @Component annotation is used for performing the dependency injection as it enables the selected module.
E.g.
@Component(modules = {ClassName.class })
public interface InterfaceName{
void inject(YourActivity acivity);
}
@Scope annotation
@Scope enables you to handle scoping at granular level through custom annotations.
@Qualifier annotation
@Qualifier is used when the type of class is actually not sufficient to identify the dependency.
Few of the Dagger 2 Advantages
* It simplifies the configuration of complex dependencies
* Generates easy to understand clean code
* Proclaim effective dependency graph
* Keeps you free from writing the substantial amount of boilerplate code
* Makes the refactoring easy as you do not need to bother about the Module order
* Easy testing as you can quickly switch Modules
* Painless access to all or the shared instances
Now, I am sharing an example of implementing Dagger 2 for a real world application in combination of Retrofit library and MVP (Model View Presenter) to explore Dagger 2 attributes such as injecting abstractions as collaborators to avoid changes in codebase and reusability of the components.
How to implement Network Call using the combination of Dagger 2, Retrofit library, and MVP (Model View Presenter)
There are several possibilities to handle the Network call for a mobile application. During my explorations for total standard architecture that also can lead to maximum reusability of code structures, I found the combination of Dagger 2, Retrofit library, and MVP (Model View Presenter) to be the best fit. Please find the details about very quick technical execution of implementation.
Retrofit: One of the most popular and efficient HTTP Client Libraries for Android. It much known for its simplicity and performance.
MVP – Model-View-Presenter Pattern
Details about the roles of its components:
→ Model — Model is the data layer mainly handles the business logic and communication with the network as well as database layers
→ View — View is the User Interface layer to display data and to notify the Presenter about various user actions
→ Presenter — Presenter is meant for retrieving the data from the Model. It also handles the state of Views as well as reacts to user input notifications
To make it easy to understand, I have divided the entire configuration process in four steps.
[1] Basic Configuration
[2] Network Module Configuration
[3] Model Class Configuration
[4] Activity level Configuration
[1] Basic Configuration
→ Initiate Configuration
In app level build.gradle file: Needs to add gradle dependencies in app level build.gradle
dependencies {
implementation 'com.google.dagger:dagger:2.5'
annotationProcessor 'com.google.dagger:dagger-compiler:2.5'
// Used for retrofit
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
}
- In Manifest file: Add internet permission.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
→ Create one interface for “inject”
@Singleton
@Component(modules = {NetworkModule.class,})
public interface Idagger {
void inject(DashBaordActivity dashBaordActivity);
}
→ Create BaseActivity for creating Dagger builder
public class DaggerApplication extends AppCompatActivity{
Idagger idagger;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
File cacheFile = new File(getCacheDir(), "responses");
idagger = DaggerIdagger.builder().networkModule(new NetworkModule(cacheFile)).build();
}
public Idagger getIdagger() {
return idagger;
}
}
→ Now you can extend DaggerApplication class in to your Activity
public class DashBaordActivity extends DaggerApplication implements DashboardView {
@Inject
public Service service;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getIdagger().inject(this);
}}
[2] Network Module Configuration
→ Create Network service interface
public interface NetworkService {
@GET("v1/ApiList")
ArrayList<ListDataResponse> getDataList();
@GET("v1/list")
Observable<ListDataResponse> getList();
}
→ Create service class
public class Service {
private final NetworkService networkService;
public Service(NetworkService networkService) {
this.networkService = networkService;
}
public ArrayList<ListDataResponse> getDataList() {
return networkService.getDataList();
}
public Subscription getList(final GetListCallback callback) {
return networkService.getList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorResumeNext(new Func1<Throwable, Observable<? extends ListDataResponse>>() {
@Override
public Observable<? extends ListDataResponse> call(Throwable throwable) {
return Observable.error(throwable);
}
})
.subscribe(new Subscriber<ListDataResponse>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
callback.onError(new NetworkError(e));
}
@Override
public void onNext(ListDataResponse listDataResponse) {
callback.onSuccess(listDataResponse);
}
});
}
public interface GetListCallback{
void onSuccess(ListDataResponse listDataResponse);
void onError(NetworkError networkError);
}
}
→ Response
public class Response {
@SerializedName("status")
public String status;
public void setStatus(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
@SuppressWarnings({"unused", "used by Retrofit"})
public Response() {
}
public Response(String status) {
this.status = status;
}
}
→ Module – NetworkModule
@Module
public class NetworkModule {
File cacheFile;
public NetworkModule(File cacheFile) {
this.cacheFile = cacheFile;
}
@Provides
@Singleton
Retrofit provideCall() {
Cache cache = null;
try {
cache = new Cache(cacheFile, 10 * 1024 * 1024);
} catch (Exception e) {
e.printStackTrace();
}
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request original = chain.request();
// Customize the request
Request request = original.newBuilder()
.header("Content-Type", "application/json")
.removeHeader("Pragma")
.header("Cache-Control", String.format("max-age=%d", BuildConfig.CACHETIME))
.build();
okhttp3.Response response = chain.proceed(request);
response.cacheResponse();
// Customize or return the response
return response;
}
})
.cache(cache)
.build();
return new Retrofit.Builder()
.baseUrl(BuildConfig.BASEURL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
}
@Provides
@Singleton
public NetworkService providesNetworkService(
Retrofit retrofit) {
return retrofit.create(NetworkService.class);
}
@Provides
@Singleton
public Service providesService(
NetworkService networkService) {
return new Service(networkService);
}
}
→ Handling Error handling – NetworkError
public class NetworkError extends Throwable {
public static final String DEFAULT_ERROR_MESSAGE = "Something went wrong! Please try again.";
public static final String NETWORK_ERROR_MESSAGE = "Please Check Your Internet Connection!";
private static final String ERROR_MESSAGE_HEADER = "Error-Message";
private final Throwable error;
public NetworkError(Throwable e) {
super(e);
this.error = e;
}
public String getMessage() {
return error.getMessage();
}
public boolean isAuthFailure() {
return error instanceof HttpException &&
((HttpException) error).code() == HTTP_UNAUTHORIZED;
}
public boolean isResponseNull() {
return error instanceof HttpException && ((HttpException) error).response() == null;
}
public String getAppErrorMessage() {
if (this.error instanceof IOExcep
tion) return NETWORK_ERROR_MESSAGE;
if (!(this.error instanceof HttpException)) return DEFAULT_ERROR_MESSAGE;
retrofit2.Response<?> response = ((HttpException) this.error).response();
if (response != null) {
String status = getJsonStringFromResponse(response);
if (!TextUtils.isEmpty(status)) return status;
Map<String, List<String>> headers = response.headers().toMultimap();
if (headers.containsKey(ERROR_MESSAGE_HEADER))
return headers.get(ERROR_MESSAGE_HEADER).get(0);
}
return DEFAULT_ERROR_MESSAGE;
}
protected String getJsonStringFromResponse(final retrofit2.Response<?> response) {
try {
String jsonString = response.errorBody().string();
Response errorResponse = new Gson().fromJson(jsonString, Response.class);
return errorResponse.status;
} catch (Exception e) {
return null;
}
}
public Throwable getError() {
return error;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NetworkError that = (NetworkError) o;
return error != null ? error.equals(that.error) : that.error == null;
}
@Override
public int hashCode() {
return error != null ? error.hashCode() : 0;
}
}
[3] Model Class Configuration
→ ListData
@Generated("org.jsonschema2pojo")
public class ListData {
@SerializedName("id")
@Expose
private String id;
@SerializedName("name")
@Expose
private String name;
@SerializedName("description")
@Expose
private String description;
@SerializedName("background")
@Expose
private String background;
/**
*
* @return
* The id
*/
public String getId() {
return id;
}
/**
*
* @param id
* The id
*/
public void setId(String id) {
this.id = id;
}
/**
*
* @return
* The name
*/
public String getName() {
return name;
}
/**
*
* @param name
* The name
*/
public void setName(String name) {
this.name = name;
}
/**
*
* @return
* The description
*/
public String getDescription() {
return description;
}
/**
*
* @param description
* The description
*/
public void setDescription(String description) {
this.description = description;
}
/**
*
* @return
* The background
*/
public String getBackground() {
return background;
}
/**
*
* @param background
* The background
*/
public void setBackground(String background) {
this.background = background;
}
}
→ ListDataResponse
@Generated("org.jsonschema2pojo")
public class ListDataResponse {
@SerializedName("data")
@Expose
private List<ListData> data = new ArrayList<ListData>();
@SerializedName("message")
@Expose
private String message;
@SerializedName("status")
@Expose
private int status;
/**
*
* @return
* The data
*/
public List<ListData> getData() {
return data;
}
/**
*
* @param data
* The data
*/
public void setData(List<ListData> data) {
this.data = data;
}
/**
*
* @return
* The message
*/
public String getMessage() {
return message;
}
/**
*
* @param message
* The message
*/
public void setMessage(String message) {
this.message = message;
}
/**
*
* @return
* The status
*/
public int getStatus() {
return status;
}
/**
*
* @param status
* The status
*/
public void setStatus(int status) {
this.status = status;
}
}
[4] Activity Level Configuration
→ DashBaordActivity
public class DashBaordActivity extends DaggerApplication implements DashboardView {
private RecyclerView list;
@Inject
public Service service;
ProgressBar progressBar;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getIdagger().inject(this);
renderView();
init();
DashBoardPresenter presenter = new DashBoardPresenter(service, this);
presenter.getDataList();
}
public void renderView(){
setContentView(R.layout.activity_home);
list = findViewById(R.id.list);
progressBar = findViewById(R.id.progress);
}
public void init(){
list.setLayoutManager(new LinearLayoutManager(this));
}
@Override
public void showWait() {
progressBar.setVisibility(View.VISIBLE);
}
@Override
public void removeWait() {
progressBar.setVisibility(View.GONE);
}
@Override
public void onFailure(String appErrorMessage) {
}
@Override
public void getDataListSuccess(ListDataResponse listDataResponse) {
BasicAdapter adapter = new BasicAdapter(getApplicationContext(), listDataResponse.getData(),
new BasicAdapter.OnItemClickListener() {
@Override
public void onClick(ListData Item) {
Toast.makeText(getApplicationContext(), Item.getName(),
Toast.LENGTH_LONG).show();
}
});
list.setAdapter(adapter);
}
}
→ DashBoardPresenter
public class DashBoardPresenter {
private final Service service;
private final DashboardView view;
private CompositeSubscription subscriptions;
public DashBoardPresenter(Service service, DashboardView view) {
this.service = service;
this.view = view;
this.subscriptions = new CompositeSubscription();
}
public void getDataList() {
view.showWait();
Subscription subscription = service.getList(new Service.GetListCallback() {
@Override
public void onSuccess(ListDataResponse ListResponse) {
view.removeWait();
view.getDataListSuccess(ListResponse);
}
@Override
public void onError(NetworkError networkError) {
view.removeWait();
view.onFailure(networkError.getAppErrorMessage());
}
});
subscriptions.add(subscription);
}
public void onStop() {
subscriptions.unsubscribe();
}
}
→ DashboardView
public interface DashboardView {
void showWait();
void removeWait();
void onFailure(String appErrorMessage);
void getDataListSuccess(ListDataResponse listDataResponse
→ Adapter BasicAdapter
public class BasicAdapter extends RecyclerView.Adapter<BasicAdapter.ViewHolder> {
private final OnItemClickListener listener;
private List<ListData> data;
private Context context;
public BasicAdapter(Context context, List<ListData> data, OnItemClickListener listener) {
this.data = data;
this.listener = listener;
this.context = context;
}
@Override
public BasicAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_home, null);
view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT));
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(BasicAdapter.ViewHolder holder, int position) {
holder.click(data.get(position), listener);
holder.tvCity.setText(data.get(position).getName());
holder.tvDesc.setText(data.get(position).getDescription());
String images = data.get(position).getBackground();
Glide.with(context)
.load(images)
.into(holder.background);
}
@Override
public int getItemCount() {
return data.size();
}
public interface OnItemClickListener {
void onClick(ListData Item);
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView tvCity, tvDesc;
ImageView background;
public ViewHolder(View itemView) {
super(itemView);
tvCity = itemView.findViewById(R.id.city);
tvDesc = itemView.findViewById(R.id.hotel);
background = itemView.findViewById(R.id.image);
}
public void click(final ListData listData, final OnItemClickListener listener) {
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onClick(listData);
}
});
}
}
Conclusion
In this blog, I have mentioned the in depth information about Dagger 2 annotations along with process of implementing Dagger 2 aligned with Retrofit library and MVP (Model View Presenter) to handle Network calls in a quick manner.
Dagger 2 enables the generation of clean, reusable, and maintainable code. Due to advances, level of functionalities, and expectations of high-speed performance, modern mobile application demand much resourceful efforts from developers. Of course, the mix blend of latest technologies acts as a powerful resources to make life easy.
If you want to learn more about Dagger 2, feel free to contact me at www.azilen.com.