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;
 
    ...
}

Ссылки