[startandroid] Dagger 2 - Урок 4

Урок 4. Producers

06 октября 2016

В этом уроке разберем асинхронные механизмы Dagger: ProductionComponent, ProducerModule, Produces, Producer. А также разберем как с помощью Produced обрабатывать ошибки.

По аналогии с аннотациями @Module, @Provides, и @Component даггер предоставляет аннотации @ProducerModule, @Produces, and @ProductionComponent. Компоненты и модули с такими аннотациями будут работать асинхронно, т.е. создавать объект в другом потоке. Для обеспечения асинхронности, компонент возвращает нам не сам объект, а ListenableFuture, на который вы можете повесить колбэк.

Чтобы использовать Produced-аннотации, добавьте в build.gradle, в список dependencies строку:

compile ‘com.google.dagger:dagger-producers:2.7’

Рассмотрим пример, в котором мы от сервера будем получать данные юзера. Т.е. у нас есть объект User, а в NetworkUtils есть метод getUserData(User user), который получит от сервера данные по указанному юзеру. Получение данных от сервера может занять некоторое время, поэтому будем использовать асинхронные механизмы даггера, чтобы не блочить UI-поток.

Содержимое модуля:

@ProducerModule(includes = NetworkModule.class)
public class UserDataModule {
 
    User mUser;
 
    public UserDataModule(User user) {
        mUser = user;
    }
 
    @Produces
    UserData getUserData(NetworkUtils networkUtils) {
        return networkUtils.getUserData(mUser);
    }
     
}

Обратите внимание, что используем аннотации ProducerModule и Produces вместо Module и Provides.

Модуль предоставляет объект UserData. Для этого он вызывает метод NetworkUtils.getUserData. Объект NetworkUtils будет получен из модуля NetworkModule (указан в include). Чтобы выполнить код в другом потоке, компоненту потребуется Executor. Его необходимо предоставить в обычном модуле

@Module
public class ExecutorModule {
    @Provides
    @Production
    static Executor executor() {
        return Executors.newSingleThreadExecutor();
    }
}

К аннотации Provides необходимо добавить аннотацию Production. Это даст понять даггеру, что он может использовать этот Executor для своих асинхронных целей.

Компонент с аннотацией ProductionComponent:

@ProductionComponent(modules={UserDataModule.class, ExecutorModule.class})
public interface UserComponent {
     
    ListenableFuture<UserData> getUserData();
     
}

В нем описан метод getUserData, который возвращает UserData, обернутый в ListenableFuture

Осталось в Activity получить ListenableFuture от компонента, и повесить на него колбэк:

User user = new User();
UserComponent userComponent = DaggerUserComponent.builder().userDataModule(new UserDataModule(user)).build();
 
ListenableFuture<UserData> listenableFutureUserData = userComponent.getUserData();
 
Futures.addCallback(listenableFutureUserData, new FutureCallback<UserData>() {
    @Override
    public void onSuccess(UserData result) {
 
    }
 
    @Override
    public void onFailure(Throwable t) {
 
    }
});

В метод onSuccess придет нужный нам объект, а onFailure вызовется в случае какого-либо Exception, возникшего в процессе создания объекта. И т.к. все это выполнялось асинхронно, то и результат придет не в main потоке.

Produced

Ошибки можно обрабатывать и до того, как они придут в onFailure метод вашего колбэка. Для этого можно использовать провайдер Produced.

Немного расширим предыдущий пример. Допустим, теперь нам необходимо сначала скачать с сервера json и распарсить его, чтобы получить UserData.

@ProducerModule(includes = NetworkModule.class)
public class UserDataModule {
 
    User mUser;
 
    public UserDataModule(User user) {
        mUser = user;
    }
 
    @Produces
    String getUserDataJson(NetworkUtils networkUtils) {
        return networkUtils.getUserDataJson(mUser);
    }
 
    @Produces
    UserData getUserData(String userDataJson) {
        return UserData.parseFromJson(userDataJson);
    }
}

Когда мы попросим компонент дать нам UserData, компонент вызовет getUserData, увидит, что там нужен String (т.е. json), и вызовет getUserDataJson, чтобы получить String. Если при вызове getUserDataJson произойдет IOException, то в ListenableFuture вместо onSuccess будет вызван метод onFailure.

Даггер предоставляет возможность обработать ошибку прямо в методе getUserData. Перепишем пример с использованием Produced.

@ProducerModule(includes = NetworkModule.class)
public class UserDataModule {
 
    User mUser;
 
    public UserDataModule(User user) {
        mUser = user;
    }
 
    @Produces
    String getUserDataJson(NetworkUtils networkUtils) throws IOException {
        return networkUtils.getUserDataJson(mUser);
    }
 
    @Produces
    UserData getUserData(Produced<String> userDataJson) {
        try {
            return UserData.parseFromJson(userDataJson.get());
        } catch (ExecutionException ex) {
            return UserData.WRONG_USER;
        }
    }
 
}

Мы обернули String в Produced, и он теперь при вызове метода get вернет объект, либо выкинет ExecutionException. Т.е. метод getUserDataJson будет вызван только при вызове get. И если произойдет ошибка, мы прямо здесь ее обрабатываем, и в ListenableFuture будет вызван метод onSuccess, в который придет объект UserData.WRONG_USER.

ExecutionException - это просто обертка, из которой вы сможете вытащить реальный Exception, который произошел во время работы.

Producer

Аналогичен Lazy. Если компонент возвращает объект, обернутый в Producer, то создание объекта начнется только при вызове метода get.

В хелпе есть отличный пример, который демонстрирует смысл Producer

@Produces
ListenableFuture<Heater> getHeater(
        HeaterFlag flag,
        @Electric Producer<Heater> electricHeater,
        @Gas Producer<Heater> gasHeater) {
    return flag.useElectricHeater() ? electricHeater.get() : gasHeater.get();
}

Методу getHeater приходит флаг и два Producer. И в зависимости от флага, метод вызывает метод get у одного из Producer. В итоге создается только один Heater-объект, т.к. процесс создания объекта начинается только при вызове метода get.