Данная статья является шестой частью серии статей, предназначенных, по словам автора, для тех, кто не может разобраться с внедрением зависимостей и фреймворком Dagger 2, либо только собирается это сделать. Оригинал написан 23 декабря 2017 года. Перевод вольный.
Это шестая статья цикла «Dagger 2 для начинающих Android разработчиков.». Если вы не читали предыдущие, то вам сюда.
@Module
и @Provides
.RecyclerView
. Я не буду тратить много времени на объяснение проекта, буду объяснять абстрактно. Но, пожалуйста, разбирайте код внимательно, чтобы внедрение Dagger 2 в проект проходило для вас максимально понятно и просто.MainActivity.java
— Делает запросы к API и показывает полученные элементы в RecyclerView
.Model
— POJO для ответа от API, создан с использованием JSON Schema to POJORandomUsersAdapter.java
— Adapter
для RecyclerView
Retrofit
— для вызовов APIGsonBuilder
и Gson
— для работы с JSONHttpLoggingInterceptor?
— для логирования сетевых операцийOkHttpClient
— клиент для Retrofit
Picasso
— работа с изображениями в Adapter
MainActivity
присутствуют зависимости. И каждый раз при создании MainActivity
экземпляры зависимостей будут создаваться снова и снова.public class MainActivity extends AppCompatActivity {
Retrofit retrofit;
RecyclerView recyclerView;
RandomUserAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();
Timber.plant(new Timber.DebugTree());
HttpLoggingInterceptor httpLoggingInterceptor = new
HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(@NonNull String message) {
Timber.i(message);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient()
.newBuilder()
.addInterceptor(httpLoggingInterceptor)
.build();
retrofit = new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://randomuser.me/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
populateUsers();
}
private void initViews() {
recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
}
private void populateUsers() {
Call<RandomUsers> randomUsersCall = getRandomUserService().getRandomUsers(10);
randomUsersCall.enqueue(new Callback<RandomUsers>() {
@Override
public void onResponse(Call<RandomUsers> call, @NonNull Response<RandomUsers> response) {
if(response.isSuccessful()) {
mAdapter = new RandomUserAdapter();
mAdapter.setItems(response.body().getResults());
recyclerView.setAdapter(mAdapter);
}
}
@Override
public void onFailure(Call<RandomUsers> call, Throwable t) {
Timber.i(t.getMessage());
}
});
}
public RandomUsersApi getRandomUserService(){
return retrofit.create(RandomUsersApi.class);
}
}
MainActivity
, то заметите следующие проблемы:onCreate()
, то можете найти неуклюжими инициализации внутри него. Мы, конечно, можем продолжать таким образом инициализировать объекты в этом методе, но лучше отыскать верный путь для решения проблемы.Picasso
внутри Adapter
тоже мешает возможности тестирования. Было бы неплохо передавать эту зависимость через конструктор.public class RandomUserAdapter extends RecyclerView.Adapter<RandomUserAdapter.RandomUserViewHolder> {
private List<Result> resultList = new ArrayList<>();
public RandomUserAdapter() {
}
@Override
public RandomUserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_random_user,
parent, false);
return new RandomUserViewHolder(view);
}
@Override
public void onBindViewHolder(RandomUserViewHolder holder, int position) {
Result result = resultList.get(position);
holder.textView.setText(String.format("%s %s", result.getName().getFirst(),
result.getName().getLast()));
Picasso.with(holder.imageView.getContext())
.load(result.getPicture().getLarge())
.into(holder.imageView);
}
......
MainActivity
, нужны были лишь для того, чтобы вы немного вникли в проект и почувствовали себя комфортно. Если углубиться, то как в любом реальном проекте зависимостей станет больше. Давайте добавим ещё несколько.File
— для хранения кэшаCache
— для сетевого кешаOkHttp3Downloader
— загрузчик, использующий OkHttpClient
для загрузки изображенийPicasso
— для обработки изображений из сетиpublic class MainActivity extends AppCompatActivity {
Retrofit retrofit;
RecyclerView recyclerView;
RandomUserAdapter mAdapter;
Picasso picasso;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();
Timber.plant(new Timber.DebugTree());
File cacheFile = new File(this.getCacheDir(), "HttpCache");
cacheFile.mkdirs();
Cache cache = new Cache(cacheFile, 10 * 1000 * 1000); //10 MB
HttpLoggingInterceptor httpLoggingInterceptor = new
HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(@NonNull String message) {
Timber.i(message);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient()
.newBuilder()
.cache(cache)
.addInterceptor(httpLoggingInterceptor)
.build();
OkHttp3Downloader okHttpDownloader = new OkHttp3Downloader(okHttpClient);
picasso = new Picasso.Builder(this).downloader(okHttpDownloader).build();
retrofit = new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://randomuser.me/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
populateUsers();
}
private void initViews() {
recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
}
private void populateUsers() {
Call<RandomUsers> randomUsersCall = getRandomUserService().getRandomUsers(10);
randomUsersCall.enqueue(new Callback<RandomUsers>() {
@Override
public void onResponse(Call<RandomUsers> call, @NonNull Response<RandomUsers> response) {
if(response.isSuccessful()) {
mAdapter = new RandomUserAdapter(picasso);
mAdapter.setItems(response.body().getResults());
recyclerView.setAdapter(mAdapter);
}
}
@Override
public void onFailure(Call<RandomUsers> call, Throwable t) {
Timber.i(t.getMessage());
}
});
}
public RandomUsersApi getRandomUserService(){
return retrofit.create(RandomUsersApi.class);
}
}
Picasso
две зависимости — OkHttp3Downloader
и Context
.Retrofit
. Ему, в свою очередь, нужны две зависимости — GsonConvertFactory
и OkHttpClient
и так далее.MainActivity
и сравнить его с диаграммой для лучшего понимания.RandomUsersAPI
и RandomUsersApi
— это одно и тоже. Просто опечатка в диаграмме.RandomUsersComponent
будет отличаться в ветке выше и примерах ниже. Предлагаю вам использовать для полного соответствия с примерами ниже новую ветку, а вышеупомянутую оставить для справки.build.gradle
файл.dependencies {
implementation 'com.google.dagger:dagger:2.13'
annotationProcessor 'com.google.dagger:dagger-compiler:2.13'
}
RandomUsersAPI
и Picasso
.
@Component
public interface RandomUserComponent {
RandomUsersApi getRandomUserService();
Picasso getPicasso();
}
RandomUsersAPI
и Picasso
? Воспользуемся модулями.MainActivity
в различные модули. Смотря на граф зависимостей можно решить какие модули необходимы. RandomUsersModule?
, он предоставит зависимости RandomUsersApi
, GsonConverterFactory
, Gson
и Retrofit
.@Module
public class RandomUsersModule {
@Provides
public RandomUsersApi randomUsersApi(Retrofit retrofit){
return retrofit.create(RandomUsersApi.class);
}
@Provides
public Retrofit retrofit(OkHttpClient okHttpClient,
GsonConverterFactory gsonConverterFactory, Gson gson){
return new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://randomuser.me/")
.addConverterFactory(gsonConverterFactory)
.build();
}
@Provides
public Gson gson(){
GsonBuilder gsonBuilder = new GsonBuilder();
return gsonBuilder.create();
}
@Provides
public GsonConverterFactory gsonConverterFactory(Gson gson){
return GsonConverterFactory.create(gson);
}
}
PicassoModule
, который предоставит зависимости Picasso
и OkHttp3Downloader
.@Module
public class PicassoModule {
@Provides
public Picasso picasso(Context context, OkHttp3Downloader okHttp3Downloader){
return new Picasso.Builder(context).
downloader(okHttp3Downloader).
build();
}
@Provides
public OkHttp3Downloader okHttp3Downloader(OkHttpClient okHttpClient){
return new OkHttp3Downloader(okHttpClient);
}
}
RandomUsersModule
для Retrofit
нужен OkHttpClient
. Которому, в свою очередь, нужны другие зависимости. Почему бы не сделать для этого отдельный модуль?OkHttpClientModule
, который предоставит OkHttpCkient
, Cache
, HttpLoggingInterceptor
и File
.@Module
public class OkHttpClientModule {
@Provides
public OkHttpClient okHttpClient(Cache cache, HttpLoggingInterceptor httpLoggingInterceptor){
return new OkHttpClient()
.newBuilder()
.cache(cache)
.addInterceptor(httpLoggingInterceptor)
.build();
}
@Provides
public Cache cache(File cacheFile){
return new Cache(cacheFile, 10 * 1000 * 1000); //10 MB
}
@Provides
public File file(Context context){
File file = new File(context.getCacheDir(), "HttpCache");
file.mkdirs();
return file;
}
@Provides
public HttpLoggingInterceptor httpLoggingInterceptor(){
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Timber.d(message);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return httpLoggingInterceptor;
}
}
PicassoModule
и OkHttpClientModule
требуется Context
и, возможно, он пригодится нам в других местах. Сделаем модуль и для этих целей.@Module
public class ContextModule {
Context context;
public ContextModule(Context context){
this.context = context;
}
@Provides
public Context context(){ return context.getApplicationContext(); }
}
Context
в другие модули? Нам нужно связать модули, которые зависят друг от друга.includes
. Этот атрибут включает в текущий модуль зависимости модулей, на которые указана ссылка.RandomUsersModule
нуждается в OkHttpClientModule
OkHttpClientModule
нуждается в ContextModule
PicassoModule
нуждается в OkHttpClientModule
и ContextModule
. Так как OkHttpClientModule
уже связан с ContextModule
, то можно обойтись только OkHttpClientModule
// в RandomUsersModule.java
@Module(includes = OkHttpClientModule.class)
public class RandomUsersModule { ... }
// в OkHttpClientModule.java
@Module(includes = ContextModule.class)
public class OkHttpClientModule { ... }
// в PicassoModule.java
@Module(includes = OkHttpClientModule.class)
public class PicassoModule { ... }
includes
, подобным образом мы можем связать компонент и модули с помощью атрибута modules
.getRandomUserService()
и getPicasso()
) включим в компонент ссылки на модули RandomUsersModule
и PicassoModule
, используя атрибут modules
.@Component(modules = {RandomUsersModule.class, PicassoModule.class})
public interface RandomUserComponent {
RandomUsersApi getRandomUserService();
Picasso getPicasso();
}
MainActivity
можно удобно получить зависимости Picasso
и RandomUsersApi
с помощью RandomUserComponent
.public class MainActivity extends AppCompatActivity {
RandomUsersApi randomUsersApi;
Picasso picasso;
....
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
RandomUserComponent daggerRandomUserComponent = DaggerRandomUserComponent.builder()
.contextModule(new ContextModule(this))
.build();
picasso = daggerRandomUserComponent.getPicasso();
randomUsersApi = daggerRandomUserComponent.getRandomUserService();
populateUsers();
...
}
...
}
DaggerComponent.build()
создаются новые экземпляры всех объектов или зависимостей, которые вы настроили. В этом случае почему Dagger 2 не знает о том, что мне нужен только один экземпляр Picasso
? Другими словами, как мы можем сказать Dagger 2 предоставлять нам зависимость как singleton?@Scope
@Scope
говорит Dagger 2 создавать только единственный экземпляр, даже если DaggerComponent.build()
вызывается многократно. Это заставляет зависимость работать как singleton. Для настройки требуемой области (Scope) необходимо создать собственную аннотацию.@Scope
@Retention(RetentionPolicy.CLASS)
public @interface RandomUserApplicationScope {
}
@Retention
— это аннотация для обозначения точки отклонения использования аннотации. Она говорит о том, когда может быть использована аннотация. Например, с отметкой SOURCE аннотация будет доступна только в исходном коде и будет отброшена во время компиляции, с отметкой CLASS аннотация будет доступна во время компиляции, но не во время работы программы, с отметкой RUNTIME аннотация будет доступна и во время выполнения программы.@RandomUserApplicationScope
@Component(modules = {RandomUsersModule.class, PicassoModule.class})
public interface RandomUserComponent { ...}
@Module(includes = OkHttpClientModule.class)
public class PicassoModule {
...
@RandomUserApplicationScope
@Provides
public Picasso picasso(Context context, OkHttp3Downloader okHttp3Downloader){
return new Picasso.Builder(context).
downloader(okHttp3Downloader).
build();
}
...
}
@Module(includes = OkHttpClientModule.class)
public class RandomUsersModule {
...
@RandomUserApplicationScope
@Provides
public Retrofit retrofit(OkHttpClient okHttpClient,
GsonConverterFactory gsonConverterFactory, Gson gson){
return new Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://randomuser.me/")
.addConverterFactory(gsonConverterFactory)
.build();
}
...
}
ApplicationContext
и контекст Activity
. Как их предоставить? Для предоставления ApplicationContext
можно использовать ContextModule
. Давайте создадим ещё один модуль.@Module
public class ActivityModule {
private final Context context;
ActivityModule(Activity context){
this.context = context;
}
@RandomUserApplicationScope
@Provides
public Context context(){ return context; }
}
Context
и Dagger 2 не сможет понять каким воспользоваться, возникнет ошибка.@Named
@Module
public class ActivityModule {
....
@Named("activity_context")
@RandomUserApplicationScope
@Provides
public Context context(){ return context; }
}
@Module
public class ContextModule {
....
@Named("application_context")
@RandomUserApplicationScope
@Provides
public Context context(){ return context.getApplicationContext(); }
}
@Module(includes = ContextModule.class)
public class OkHttpClientModule {
....
@Provides
@RandomUserApplicationScope
public File file(@Named("application_context") Context context){
File file = new File(context.getCacheDir(), "HttpCache");
file.mkdirs();
return file;
}
....
}
@Module(includes = OkHttpClientModule.class)
public class PicassoModule {
...
@RandomUserApplicationScope
@Provides
public Picasso picasso(@Named("application_context")Context context, OkHttp3Downloader okHttp3Downloader){
return new Picasso.Builder(context).
downloader(okHttp3Downloader).
build();
...
}
@Named
— @Qualifier
@Named
на @Qualifier
нужно создать отдельную аннотацию и использовать её где необходимо.@Qualifier
public @interface ApplicationContext {}
@Module
public class ContextModule {
....
@ApplicationContext
@RandomUserApplicationScope
@Provides
public Context context(){ return context.getApplicationContext(); }
}
ApplicationContext
созданной аннотацией.@Module(includes = ContextModule.class)
public class OkHttpClientModule {
...
@Provides
@RandomUserApplicationScope
public File file(@ApplicationContext Context context){
File file = new File(context.getCacheDir(), "HttpCache");
file.mkdirs();
return file;
}
....
}
@Module(includes = OkHttpClientModule.class)
public class PicassoModule {
@RandomUserApplicationScope
@Provides
public Picasso picasso(@ApplicationContext Context context, OkHttp3Downloader okHttp3Downloader){
return new Picasso.Builder(context).
downloader(okHttp3Downloader).
build();
}
....
}
@Named
на @Qualifier
.@Scope
, для получения зависимостей в единственном экземпляре. Вторая — @Named
, для разделения методов, предоставляющих зависимости одинакового типа. Третья — @Qualifier
, как альтернатива @Named
.Activity
, создадим несколько компонентов и научимся работать с ними. Следующая статья выйдет через неделю.
К сожалению, не доступен сервер mySQL