Статьи

Компромиссы при внедрении зависимостей во время компиляции в Android

Как бэкэнд-разработчик программного обеспечения, я привык к Spring как к своему любимому движку Dependency Injection. Альтернативы включают CDI Java EE, который достигает того же результата — другим способом. Однако оба метода внедряются во время выполнения: это означает, что при запуске приложения необходимо заплатить определенную цену производительности, а также время, необходимое для выполнения всех зависимостей. На сервере приложений, где продолжительность жизни приложения измеряется днями (если не неделями), накладные расходы времени запуска приемлемы. Это даже полностью прозрачно, если сервер является узлом в большом кластере.

Как пользователь Android, я не доволен, когда запускаю приложение, и оно задерживается на несколько секунд перед открытием. Было бы очень плохо с точки зрения удобства для пользователя, если бы мы добавили еще несколько секунд к этому времени. Что еще хуже, потребление памяти от механизма DI будет катастрофой. Вот почему Square разработала механизм внедрения зависимостей во время компиляции под названием Dagger. Обратите внимание, что Dagger 2 в настоящее время разрабатывается Google. Прежде чем идти дальше, я должен признать, что документация по Dagger 2 лаконична — в лучшем случае. Но это отличная возможность для другого поста в блоге:-)

Dagger 2 работает с процессором аннотаций: при компиляции он анализирует ваш аннотированный код и создает код соединения между вашими компонентами. Хорошо, что этот код очень похож на то, что вы написали бы сами, если бы делали это вручную, секретной черной магии не существует (в отличие от DI времени выполнения и их прокси). Следующий код отображает класс для внедрения:

public class TimeSetListener implements TimePickerDialog.OnTimeSetListener {

    private final EventBus eventBus;

    public TimeSetListener(EventBus eventBus) {
        this.eventBus = eventBus;
    }

    @Override
    public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
        eventBus.post(new TimeSetEvent(hourOfDay, minute));
    }
}

Обратите внимание, что код полностью независим от Dagger во всех отношениях. Нельзя сделать вывод, как это будет введено в конце. Интересная часть состоит в том, как использовать Dagger для введения требуемой eventBusзависимости. Есть два шага:

  1. Получить ссылку на eventBusэкземпляр в контексте
  2. Вызовите конструктор с соответствующим параметром

Сама конфигурация проводки выполняется в так называемом модуле:

@Module
public class ApplicationModule {

    @Provides
    @Singleton
    public TimeSetListener timeSetListener(EventBus eventBus) {
        return new TimeSetListener(eventBus());
    }

    ...
}

Обратите внимание, что метод EventBusпередается в качестве параметра методу, и его предоставление зависит от контекста. Также сфера действия явно @Singleton.

Привязка к фабрике происходит в компоненте , который ссылается на требуемый модуль (или более):

@Component(modules = ApplicationModule.class)
@Singleton
public interface ApplicationComponent {
    TimeSetListener timeListener();
    ...
}

Это довольно просто … до тех пор, пока кто-то не заметит, что у некоторых — если не у большинства объектов в Android есть жизненный цикл, управляемый самой Android, без вызова нашего дружественного к инъекции конструктора . Действия являются такими объектами: они создаются и запускаются платформой. Только через специальные методы жизненного цикла, например, onCreate()мы можем подключить наш код к объекту. Этот вариант использования выглядит намного хуже, так как внедрение поля является обязательным. Хуже того, также необходимо вызвать Dagger: в этом случае он действует как обычная фабрика.

public class EditTaskActivity extends AbstractTaskActivity {

    @Inject TimeSetListener timeListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        DaggerApplicationComponent.create().inject(this);
    }
    ...
}

Впервые мы видим связь с Кинжалом, но она большая. Что такое DaggerApplicationComponent? Реализация первых ApplicationComponent, а также фабрика по предоставлению экземпляров из них. И поскольку он не предоставляет inject()метод, мы должны объявить его в нашем интерфейсе:

@Component(modules = ApplicationModule.class)
@Singleton
public interface ApplicationComponent {
    TimeSetListener timeListener();
    void inject(EditTaskActivity editTaskActivity);
    ...
}

Для записи, сгенерированный класс выглядит так:

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class DaggerApplicationComponent implements ApplicationComponent {
  private Provider<TimeSetListener> timeSetListenerProvider;
  private MembersInjector<EditTaskActivity> editTaskActivityMembersInjector;

  ...

  private DaggerApplicationComponent(Builder builder) {  
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {  
    return new Builder();
  }

  public static ApplicationComponent create() {  
    return builder().build();
  }

  private void initialize(final Builder builder) {  
    this.timeSetListenerProvider = ScopedProvider.create(ApplicationModule_TimeSetListenerFactory.create(builder.applicationModule, eventBusProvider));
    this.editTaskActivityMembersInjector = TimeSetListener_MembersInjector.create((MembersInjector) MembersInjectors.noOp(), timeSetListenerProvider);
  }

  @Override
  public EventBus eventBus() {  
    return eventBusProvider.get();
  }

  @Override
  public void inject(EditTaskActivity editTaskActivity) {  
    editTaskActivityMembersInjector.injectMembers(editTaskActivity);
  }

  public static final class Builder {
    private ApplicationModule applicationModule;

    private Builder() {  
    }

    public ApplicationComponent build() {  
      if (applicationModule == null) {
        this.applicationModule = new ApplicationModule();
      }
      return new DaggerApplicationComponent(this);
    }

    public Builder applicationModule(ApplicationModule applicationModule) {  
      if (applicationModule == null) {
        throw new NullPointerException("applicationModule");
      }
      this.applicationModule = applicationModule;
      return this;
    }
  }
}

Там нет такого понятия, как бесплатный обед. Несмотря на то, что DI во время компиляции на первый взгляд очень привлекателен, он становится намного меньше, когда используется для объектов, за пределами которых жизненный цикл не управляется нашим кодом. Недостатки становятся очевидными: связь с платформой DI и, что более важно, повышенная сложность для юнит-тестирования класса Однако, учитывая ограничения Android, это может быть лучшим, что может быть достигнуто.