[startandroid] Dagger 2 - Урок 1

Зачем нужен Dagger

Если вы хотите снизить зависимость объектов друг от друга и упростить написание тестов для вашего кода, то вам подойдет паттерн Dependency Injection . А Dagger - это библиотека, которая поможет в реализации этого паттерна. В этом мини-курсе я опишу использование библиотеки Dagger версии 2 (далее по тексту даггер).

Плюсы даггера в сравнении с другими библиотеками:

  • генерирует код несложный для понимания и отладки
  • проверяет зависимости на этапе компиляции
  • не создает проблем при использовании proguard

Сразу скажу, что тема нетривиальная и у вас могут возникать вопросы типа " а что будет, если сделать так? ". Все случаи я рассмотреть не могу, поэтому очень рекомендую вам создавать примеры и на них проверять как все это работает в том или ином случае. Мне практика очень сильно помогла лучше понять теорию.

Чтобы понять, зачем нам может понадобиться Dependency Injection и даггер, давайте рассмотрим небольшой абстрактный пример, в котором смоделируем ситуацию, когда создание одного объекта может повлечь за собой создание еще нескольких.

Пусть в нашем приложении есть некая MainActivity и, в соответствии с паттерном MVP, для нее есть презентер. Презентеру для работы нужны будут некие ItemController и DataController. Т.е. нам надо будет создать два этих объекта перед тем, как создать презентер. Но для создания двух этих объектов нам, в свою очередь, нужны объекты ApiService и SharedPreferences. А для создания ApiService нужны RestAdapter, RestAdapter.Builder, OkHttpClient и Cache.

В обычной реализации это может выглядеть так:

public class MainActivity extends Activity {
 
    MainActivityPresenter activityPresenter;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        File cacheDirectory = new File("some path");
        Cache cache = new HttpResponseCache(cacheDirectory, 50 * 1024 * 1024);
 
        OkHttpClient httpClient = new OkHttpClient();
        httpClient.setCache(cache);
 
        RestAdapter.Builder builder = new RestAdapter.Builder();
        builder.setClient(new OkClient(httpClient));
        RestAdapter restAdapter = builder.build();
        ApiService apiService = restAdapter.create(ApiService.class);
 
        ItemController itemController = new ItemController(apiService);
 
        SharedPreferences preference = getSharedPreferences("item_prefs", MODE_PRIVATE);
        DataController dataController = new DataController(preference);
 
        activityPresenter = new MainActivityPresenter(this, itemController, dataController);
    }
 
}

В MainActivity мы создаем кучу объектов, чтобы по итогу получить один презентер. Нам в этом примере не важно, какие именно объекты создаются. Главное - это сколько кода может потребоваться написать в MainActivity, чтобы получить результат.

Если мы применим паттерн Dependency Injection и используем даггер, то код в Activity будет выглядеть так:

public class MainActivity extends Activity {
 
    MainActivityPresenter activityPresenter;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        activityPresenter = App.getComponent().getPresenter();
    }
}

Разумеется, код создания объектов никуда не исчез. Но он вынесен из Activity в отдельные классы, к которым даггер имеет доступ. В итоге мы просто вызываем метод getPresenter чтобы получить объект MainActivityPresenter. А даггер уже сам создаст этот объект и всю необходимую для него иерархию объектов.

То же самое мы могли бы сделать и без даггера, простым переносом кода создания объектов в метод типа MainActivityPresenter.createInstance(). Но если у нас есть другой presenter, которому частично нужны те же объекты, в его методе createInstance нам придется дублировать код создания некоторых объектов.

При использовании даггера, код создания необходимого нам объекта будет существовать только в одном месте и в одном экземпляре, и даггер использует этот код везде, где потребуется создать объект.

Теория

Теперь давайте смотреть, как работает даггер изнутри.

Возьмем все тот же пример с Activity и Presenter. Т.е. когда Activity для своих нужд создает объект Presenter. Обычая схема создания будет выглядеть так:

Activity > Presenter

Т.е. Activity создает Presenter самостоятельно

При использовании даггера схема будет выглядеть так:

Activity -> Component -> Module -> Presenter

Activity обращается к компоненту, компонент с помощью модулей создает Presenter и возвращает его в Activity.

Модули и компоненты - это два ключевых понятия даггера.

Модули - это просто классы, куда мы помещаем код создания объектов. И обычно каждый модуль включает в себя объекты близкие по смыслу. Например:

Модуль ItemModule будет содержать в себе код создания объектов, связанных с пользователями, т.е. что-нибудь типа Item и ItemController.

Модуль NetworkModule - объекты OkHttpClient и ApiService.

Модуль StorageModule - объекты DataController и SharedPreferences

Компонент - это посредник между Activity и модулем. Когда Activity нужен какой-либо объект, она сообщает об этом компоненту. Компонент знает, какой модуль умеет создавать такой объект, просит модуль создать объект и передает его в Activity. При этом компонент может использовать другие модули, чтобы создать всю иерархию объектов, необходимую для создания искомого объекта.

Процесс работы даггера можно сравнить с обедом в McDonalds. Т.е. по аналогии со схемой даггера:

Activity -> Component -> Module -> Presenter

схема McDonalds выглядит так:

Клиент -> Кассир -> Производственная линия -> Заказ (Бигмак/Картошка/Кола)

Рассмотрим подробнее шаги этих схем:

McDonalds Даггер
Клиент определился, что его заказ будет состоять из бигмака, картошки и колы, и он говорит об этом кассиру Activity сообщает компоненту, что ему понадобится Presenter
Кассир ходит по производственной линии и собирает заказ: берет бигмак, наливает колу, насыпает картошку Компонент использует модули, чтобы создать все необходимые объекты, которые понадобятся для создания Presenter
Кассир комплектует заказ в пакет или на поднос и выдает его клиенту Компонент в итоге получает от модулей требуемый объект Presenter и отдает его Activity

Практика

Теперь на простом примере посмотрим, как создавать модули и компоненты, и как с их помощью Activity будет получать требуемые объекты.

Подключение даггера к проекту

Создайте новый проект. Чтобы использовать даггер, добавьте в раздел dependencies файла build.gradle вашего модуля:

compile 'com.google.dagger:dagger:2.7'
annotationProcessor 'com.google.dagger:dagger-compiler:2.7'

Если не работает, то удалите.

И попробуйте добавить в конец файла build.gradle вашего модуля строки:

// Add plugin https://bitbucket.org/hvisser/android-apt
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}
 
// Apply plugin
apply plugin: 'com.neenbedankt.android-apt'
 
// Add Dagger dependencies
dependencies {
    compile 'com.google.dagger:dagger:2.7'
    apt 'com.google.dagger:dagger-compiler:2.7'
}

Если вдруг у вас что-то не работает, то воспользуйтесь этим готовым рабочим проектом.

В качестве объектов, которые мы будем запрашивать от даггера, используем пару классов: DatabaseHelper и NetworkUtils.

public class DatabaseHelper {
   
}
public class NetworkUtils {
 
}

Их реализация нам сейчас не важна, оставляем их пустыми.

Предположим, что эти объекты будут нужны нам в MainActivity.

public class MainActivity extends Activity {
 
    DatabaseHelper databaseHelper;
    NetworkUtils networkUtils;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

Чтобы получить их с помощью даггера, нам нужно создать модули и компонент.

Создаем модули, которые будут уметь предоставлять требуемые объекты. Именно в модулях мы и пишем весь код по созданию объектов. Это обычные классы, но с парой аннотаций:

@Module
public class NetworkModule {
 
    @Provides
    NetworkUtils provideNetworkUtils() {
        return new NetworkUtils();
    }
 
}
@Module
public class StorageModule {
 
    @Provides
    DatabaseHelper provideDatabaseHelper() {
        return new DatabaseHelper();
    }
 
}

Аннотацией @Module мы сообщаем даггеру, что этот класс является модулем. А аннотация @Provides указывает, что метод является поставщиком объекта и компонент может использовать его, чтобы получить объект.
Технически можно было вполне обойтись и одним модулем. Но логичнее будет разделить объекты на модули по их смыслу и области применения.
Модули готовы, теперь создаем компонент. Для этого нам необходимо создать интерфейс

@Component()
public interface AppComponent {
 
}

Данный интерфейс описывает пустой компонент, который пока ничего не будет уметь. При компиляции проекта, даггер найдет этот интерфейс по аннотации @Component и сгенерирует класс DaggerAppComponent (имя класса = слово Dagger + имя интерфейса), которые реализует этот интерфейс. Это и будет класс компонента.

Все что от нас требуется - наполнить интерфейс методами. Этим мы дадим понять компоненту, какие объекты он должен уметь нам возвращать. А при сборе проекта даггер уже сам их реализует в сгенерированном классе компонента.

Компонент может возвращать нам объекты двумя способами. Первый - это обычные get-методы . Т.е. мы просто вызываем метод, который вернет нам объект. Второй способ интереснее, это inject-методы . В этом случае мы передаем компоненту экземпляр Activity, и компонент сам заполняет там все необходимые поля, создавая необходимые объекты.

Рассмотрим оба способа на примерах.

Get методы

Дополним интерфейс, чтобы компонент научился создавать для нас объекты.

@Component(modules = {StorageModule.class, NetworkModule.class})
public interface AppComponent {
    NetworkUtils getNetworkUtils();
    DatabaseHelper getDatabaseHelper();
}

Список modules - это модули, в которых компонент сможет искать код создания объектов.

Методы могут быть с любым именем, главное - это их возвращаемые типы (NetworkUtils и DatabaseHelper). Они дают понять компоненту, какие именно объекты мы захотим от него получить. При компиляции, даггер проверит, в каком модуле какой объект можно достать и нагенерит в реализации двух этих методов соответствующий код создания этих объектов. А в MainActivity мы просто вызовем эти методы компонента, чтобы получить готовые объекты.

Осталось где-то описать создание экземпляра компонента. Используем для этого Application класс. Не забудьте добавить его в манифест

public class App extends Application {
 
    private static AppComponent component;
 
    @Override
    public void onCreate() {
        super.onCreate();
        component = DaggerAppComponent.create();
    }
 
    public static AppComponent getComponent() {
        return component;
    }
 
}

В методе onCreate создаем компонент. На этом месте ваша среда разработки скорее всего будет ругаться на класс DaggerAppComponent. Так происходит, потому что класса DaggerAppComponent пока не существует. Мы только описали интерфейс компонента AppComponent, но нам надо скомпилировать проект, чтобы даггер создал этот класс-компонент.

Скомпилируйте проект. В Android Studio это можно сделать через меню Build -> Make Project (CTRL+F9). После того, как процесс завершится, класс DaggerAppComponent будет создан в недрах папки build\generated. Студия теперь знает этот класс и должна предлагать добавить его в import, чтобы в коде не было никаких ошибок.

Теперь в MainActivity мы можем использовать этот компонент, чтобы получить готовые объекты DatabaseHelper и NetworkUtils:

public class MainActivity extends Activity {
 
    DatabaseHelper databaseHelper;
    NetworkUtils networkUtils;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        databaseHelper = App.getComponent().getDatabaseHelper();
        networkUtils = App.getComponent().getNetworkUtils();
    }
}

При запуске приложения объекты будут созданы даггером. Если у вас крэшит с NPE, убедитесь, что добавили App класс в манифест.

Inject-методы

У нас в MainActivity сейчас всего два объекта, которые мы получаем от компонента. Но если будет штук 20, то придется в интерфейсе компонента описать 20 get-методов и в коде MainActivity написать 20 вызовов этих методов. У даггера есть более удобное решение для таких случаев. Мы можем научить компонент не просто возвращать объекты, а самому наполнять Activity требуемыми объектами. Т.е. мы даем компоненту экземпляр MainActivity, а он смотрит, какие объекты нужны, создает их и сам помещает в соответствующие поля.

Перепишем интерфейс компонента

@Component(modules = {StorageModule.class, NetworkModule.class})
public interface AppComponent {
    void injectsMainActivity(MainActivity mainActivity);
}

Вместо пары get-методов мы описываем один inject-метод. Имя может быть любым, главное - это тип его единственного параметра. Мы указываем здесь MainActivity. Тем самым, мы говорим компоненту, что когда мы будем вызывать этот метод и передавать туда экземпляр MainActivity, мы ожидаем, что компонент наполнит этот экземпляр требуемыми объектами.

При компиляции проекта, даггер увидит этот метод в интерфейсе, просмотрит класс MainActivity на наличие (помеченных специальными аннотациями) полей и определит, какие объекты ему нужно будет создавать. В итоге, в классе компонента даггер реализует метод injectsMainActivity так, чтобы он получал объекты из своих модулей и подставлял их в соответствующие переменные переданного ему экземпляра MainActivity.

Перепишем MainActivity

public class MainActivity extends Activity {
 
    @Inject
    DatabaseHelper databaseHelper;
 
    @Inject
    NetworkUtils networkUtils;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        App.getComponent().injectsMainActivity(this);
    }
     
}

Аннотациями @Inject мы помечаем поля, которые компонент должен заполнить. При вызове метода injectsMainActivity компонент вытащит из модулей объекты DatabaseHelper и NetworkUtils и поместит их в поля MainActivity

Этот механизм можно посмотреть в коде класса компонента, который был сгенерирован даггером. Метод injectsMainActivity:

public void injectsMainActivity(MainActivity mainActivity) {
    mainActivityMembersInjector.injectMembers(mainActivity);
}

Если пойти дальше и посмотреть внутрь mainActivityMembersInjector.injectMembers, увидим следующее:

@Override
public void injectMembers(MainActivity instance) {
    if (instance == null) {
        throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.databaseHelper = databaseHelperProvider.get();
    instance.networkUtils = networkUtilsProvider.get();
}

Здесь просто проверка на null и присвоение объектов в поля MainActivity.

Разумеется, get-методы и inject-методы могут быть использованы вместе в одном компоненте. Я описывал их отдельно друг от друга только для простоты понимания.

Граф зависимостей

Совокупность всех объектов, которые умеет создавать компонент, называется граф объектов компонента, или граф зависимостей компонента. Т.е. в примере выше этот граф состоит всего из двух объектов: DatabaseHelper и NetworkUtils. Компонент знает как создать эти объекты и может их предоставить.

В некоторых случаях при создании одного объекта, компоненту может потребоваться другой объект. Мы говорили об этом в самом начале этого урока. Когда для создания презентера нам понадобилось создать еще с десяток объектов.

Рассмотрим пример модуля

@Module
public class NetworkModule {
 
    @Provides NetworkUtils provideNetworkUtils(HttpClient httpClient) {
        return new NetworkUtils(httpClient);
    }
 
    @Provides HttpClient provideHttpClient() {
        return new HttpClient();
    }
 
}

Когда мы от компонента попросим объект NetworkUtils, компонент придет в этот модуль и вызовет метод provideNetworkUtils. Но на вход этому методу требуется объект HttpClient. Компонент ищет, какой из его модулей умеет создавать такой объект и находит его в этом же модуле. Он вызывает метод provideHttpClient, получает объект HttpClient и использует его при вызове provideNetworkUtils. Т.е. если ваш объект требует для создания другие объекты, то вам необходимо в модулях описать создание всех этих объектов. В этом случае компонент создаст всю цепочку и получит искомый объект.

Бывают случаи, когда не все можно так просто создать в модулях и нужны какие-то объекты извне даггера. Этот случай я опишу в одном из следующих уроков.

Выявление ошибок

К плюсам даггера относят то, что если у вас есть какая-то ошибка в построении зависимостей, то вы узнаете об этом не в Runtime, а на этапе компиляции. Давайте проверим. Создадим еще один пустой класс Preferences.

public class Preferences {
     
}

И добавим в MainActivity переменную этого типа с аннотацией Inject:

@Inject
Preferences preferences; 

Теперь компонент при инджекте должен создать объект Preferences, но мы не добавили создание этого объекта в модули. И компонент просто не знает откуда его взять.

Пытаемся скомпилировать. И получаем ошибку:
Error:(24, 10) error: Preferences cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method.

Компилятор вполне закономерно жалуется, что не знает, откуда компоненту взять объект Preferences.

Что дальше?

В этом уроке я рассказал самые основы даггера без особых подробностей и сложностей. Прочитав его, вы сможете понять общий принцип работы и роли компонента и модулей. В следующих уроках я расскажу о дополнительных возможностях, которые предоставляет даггер при создании объектов. Мы рассмотрим пример, как могут быть организованы компоненты в небольшом приложении. А также, мы изучим возможность компонента создавать объекты в отдельном потоке.

1 симпатия