Dependency Injection. Dagger 2, часть 1
Dependency Injection — паттерн, реализующий принцип объектно-ориентированного программирования Inversion of Control. Эта история о том, как с помощью Dagger 2 реализовать этот паттерн.
Я покажу, как добавить Dagger 2 к проекту и как использовать его для управления объектами. Для этого мы преобразуем синглтон в модуль Dagger‘а. Ни о самом паттерне Inversion of Control, ни о пользе от него я рассказывать не буду, только практика.
Изменение (Октябрь 2018): Я поправил примеры для того, чтобы они были актуальны для текущих
версий Dagger 2
и Android SDK
. Сама статья не выглядет устаревшей - Dagger принципиально
не изменился с тех пор, но вам может быть также интересна статья про расширение Dagger‘а
для Android. А вот две другие статьи
(“Часть 2. MVP. Тесты” и”Часть 3. Использование в тестах. Тестируем View”) я удалил: примеры безвозвратно
устарели, и они требуют скорее полного переписывания, чем обновления. Возможно, когда-нибудь представится
повод поговорить об этом еще раз.
Историческая справка
Dagger 1 был создан компанией Square (также создавшей Retrofit, Picasso и множество других прекрасных библиотек) в 2012 году и развивался до 2016 года. В 2018 году было официально объявлено о прекращении развития в пользу Dagger 2.
Dagger 2 был создан на базе Dagger 1 и активно развивается компанией Google. Идея сохранилась, но механизм был принципиально переработан: вместо рефлексии используется генерация классов на этапе компиляции.
Дальше в статье я буду говорить только о Dagger 2, который для краткости буду называть просто Dagger.
Площадка
Для начала нам нужна площадка для экспериментов. Поэтому сделаем приложение, в котором требуется долговременное хранение данных. Для этого мы сделаем:
- Пользовательский интерфейс, которое при первом запуске показывает особое сообщение
- Класс-хелпер для работы с Shared Preferences, чтобы хранить факт того, что первый запуск уже состоялся
Интерфейс будет состоять из одного TextView
:
hello = (TextView)findViewById(R.id.hello);
if (MyPreferences.getInstance().isVisited()) {
hello.setText("welcome back");
} else {
hello.setText("Hello, anonymous");
MyPreferences.getInstance().setVisited();
}
Хелпер для работы с Shared Preferences будет синглтоном, при инициализации нужен Context
:
public class MyPreferences {
public static final String PREFS_NAME = "MyPrefsFile";
private final SharedPreferences prefs;
private MyPreferences(Context context) {
prefs = context.getSharedPreferences(PREFS_NAME, 0);
}
static MyPreferences instance;
public static MyPreferences getInstance() {
return instance;
}
public synchronized static MyPreferences getInstance(Context context) {
if (instance == null) {
instance = new MyPreferences(context);
}
return instance;
}
private static final String visited = "visited";
public boolean isVisited () {
return prefs.getBoolean(visited, false);
}
public void setVisited() {
prefs.edit().putBoolean(visited, true).apply();
}
}
Чтобы не возиться с контекстом и не следить, когда первый раз будет запрошен экземпляр нашего хелпера, превентивно инициализируем его в классе нашего приложения:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
MyPreferences.getInstance(this);
}
}
Не забываем указать класс MyApplication
в AndroidManifest.xml
в тэге <application>
.
Если вы по ходу этого рассказа проводите описываемый эксперимент, то стоит проверить, что приложение собирается и работает согласно нашему замыслу.
Заводим Dagger
Необходимо подключить библиотеку Dagger 2 в файлах Gradle.
Изменение (Октябрь 2018): раньше для работы с Dagger 2 тебовалось явно подключить процессор анотаций (apt), сейчас этот функционал предоставляется Android plugin‘ом. Эти изменения учтены в примере.
Добавляем в build.gradle зависимость:
dependencies {
...
implementation "com.google.dagger:dagger:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
compileOnly 'org.glassfish:javax.annotation:10.0-b28'
}
На момент последнего редактирования статьи ext.daggerVersion = '2.17'
Сейчас можно проверить, что студия без ошибок синхронизирована изменения в build.gradle.
Модули
Модуль — это класс, описывающий Dagger‘у, как создавать различные объекты. Каждому создаваемому объекту соответствует один метод с аннотацией @Provides.
Создадим два модуля: AndroidModule
с сервисами операционной системы, и AppModule
, с сервисами и хелперами
нашего приложения. AndroidModule
будет поставлять пока только Context
:
@Module
public class AndroidModule {
Context context;
public AndroidModule(Context context) {
this.context = context;
}
@Provides
@Singleton
Context providesContext() {
return context;
}
}
AppModule
будет предоставлять экземпляр существующего хелпера настроек:
@Module
public class AppModule {
@Provides
@Singleton
MyPreferences providesMyPreferences(Context context) {
return new MyPreferences(context);
}
}
Компонент
Компонент — это структура, которая связывает модули — поставщики объектов, и классы — потребители этих объектов.
На языке программирования компонент описывается как интерфейс. Реализация этого интерфейса будет создана автоматически.
Этот компонента содержит список используемых модулей и список потребителей. Модули перечисляются в параметре
аннотации @Component
. Для каждого потребителя создается метод inject()
, этот метод будет использоваться для
передачи потребителю запрошенные объекты, пример будет ниже.
Компонент AppComponent
будет содержать два модуля (AppModule
и AndroidModule
) и поставлять объекты в нашу
единственную активити:
@Singleton
@Component(modules = {AppModule.class, AndroidModule.class})
public interface AppComponent {
void inject(MainActivity activity);
}
Необходимо выполнить Rebuild Project, чтобы создался класс данного компонента.
Теперь можно создать экземпляр компонента. Создавать его будем при старте приложения. Для того, чтобы он был доступен отовсюду, сразу создадим getter:
public class MyApplication extends Application {
private static AppComponent appComponent;
@Override
public void onCreate() {
super.onCreate();
appComponent =
DaggerAppComponent
.builder()
.androidModule(new AndroidModule(this))
.appModule(new AppModule())
.build();
}
public static AppComponent getAppComponent() {
return appComponent;
}
}
Для того, чтобы получить доступ к компоненту из активити, мы должны дописать соответствующий метод. В дальнейшем этот метод можно будет перенести в общий для всех активити абстрактный суперкласс (в моих приложениях он как правило есть). Но пока так:
public class MainActivity extends AppCompatActivity {
...
AppComponent getAppComponent() {
return ((MyApplication)getApplication()).getAppComponent();
}
}
На этом почти вся подготовка закончена. Можно пожинать плоды трудов.
Использование объектов, предоставляемых Dagger
После всех приготовлений Dagger готов по запросу создать и предоставить нужный объект. Рассмотрим, как класс-потребитель обращается к Daggger.
Для того, чтобы получить объект, нужно:
- описать поле, в которое объект будет помещен
- добавить к описанию аннотацию
@Inject
- вызвать метод
inject()
компонента.
Вот так:
public class MainActivity extends AppCompatActivity {
@Inject MyPreferences preferences;
protected void onCreate(Bundle savedInstanceState) {
...
getAppComponent().inject(this);
...
}
}
После вызова inject(this)
в поле preferences
окажется объект класса MyPreferences
. Не это ли счастье?
Все вместе:
public class MainActivity extends AppCompatActivity {
@Inject MyPreferences preferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
hello = (TextView)findViewById(R.id.hello);
getAppComponent().inject(this);
if (preferences.isVisited()) {
hello.setText("welcome back");
} else {
hello.setText("Hello, anonymous");
preferences.setVisited();
}
}
...
}
Получение объектов 2
Я думал на этом остановиться, но хочу показать еще один пример.
Мне очень не нравится постоянно писать в коде различные getSystemService(). Во-первых, надоело. Во-вторых, взаимодействие с этими сервисами сложно тестировать, потому что нарушен принцип Inversion of Control.
Хорошим решением является получение системных сервисов через Dagger.
Допустим, нам нужен NotificationManager
. Подходящий модуль у нас уже
есть, осталось его расширить:
@Module
public class AndroidModule {
...
@Provides
@Singleton
NotificationManager providesNotificationManager(Context context) {
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
}
Теперь, чтобы получить NotificationManager
в нашей активити, достаточно добавить аннотацию @Inject
к
соответствующему полю:
public class MainActivity extends AppCompatActivity {
@Inject
NotificationManager notificationManager;
...
}