Dagger 2 Multibindings

в 13:42, , рубрики: android development, dagger 2, dependency injection, Разработка под android

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

Для данной статьи необходимы базовые знания по Dagger 2. В примерах использовался Dagger версии 2.11

Dagger 2 позволяет забайндить несколько объектов в коллекцию, даже в тех случаях, когда байндинг этих объектов происходит в разных модулях. Dagger 2 поддерживает Set и Map мультибайндинг.

Set multibindings

Для того чтобы добавить элемент в Set, достаточно добавить аннотацию @IntoSet над @Provides методом в модуле:

@Module
public class ModuleA {

    @IntoSet
    @Provides
    public FileExporter xmlFileExporter(Context context) {
        return new XmlFileExporter(context);
    }
}

@Module
public class ModuleB {

   @IntoSet
   @Provides
    public FileExporter provideCSVFileExporter(Context context) {
        return new CSVFileExporter(context);
    }
}

Добавим два этих модуля в наш компонент:

@Component(modules = {ModuleA.class, ModuleB.class})
public interface AppComponent {
     //inject methods
}

Т.к. мы объединили наши два модуля, которые содержат байндинг элементов в сет в один компонент, даггер объединит эти элементы в одну коллекцию:

public class Values {

    @Inject
    public Values(Set<FileExporter> values) {
         //значения в values: [XmlFileExporter, CSVFileExporter]
    }
}

Мы также можем добавить несколько элементов за один раз, для этого нам надо, чтобы наш @Provide метод имел возвращаемый тип Set и поставить аннотацию @ElementsIntoSet над @Provide методом.

Заменим наш ModuleB:

@Module
public class ModuleB {

   @ElementsIntoSet
   @Provides
    public Set<FileExporter> provideFileExporters(Context context) {
        return new HashSet<>(Arrays.asList(new CSVFileExporter(context),
                new JSONFileExporter(context)));
    }
}

Результат:

public class Values {

    @Inject
    public Values(Set<FileExporter> values) {
         //значения в values: [XmlFileExporter, CSVExporter, JSONFileExporter]
    }
}

Можно предоставлять зависимость через компонент:

@Component(modules = {ModuleA.class, ModuleB.class})
public interface AppComponent {
    Set<FileExporter> fileExporters();
}

      Set<FileExporter> fileExporters = DaggerAppComponent
                .builder()
                .context(this)
                .build()
                .fileExporters();

Также мы можем предоставлять коллекции с использованием @Qualifier над @Provides методом, тем самым разделять их.

Заменим еще раз наш ModuleB:

@Module
public class ModuleB {

   @ElementsIntoSet
   @Provides  
   @Named("CSV_JSON")
    public Set<FileExporter> provideFileExporters(Context context) {
        return new HashSet<>(Arrays.asList(new CSVFileExporter(context),
                new JSONFileExporter(context)));
    }
}

// Без Qualifier
public class Values {

    @Inject
    public Values(Set<FileExporter> values) { 
         //значения в values: [XmlFileExporter]. 
        //Здесь мы указали без кваливайра, поэтому
        //будут собраны объекты c ModuleA.
    }
}

// С Qualifier 
public class Values {

   @Inject
    public Values(@Named("CSV_JSON") Set<FileExporter> values) { 
            //значения в values: [CSVExporter, JSONFileExporter]
      }
}

//Через компонент
@Component(modules = {ModuleA.class, ModuleB.class})
public interface AppComponent {
    @Named("CSV_JSON")  Set<FileExporter> fileExporters();
}

Dagger 2 предоставляет возможность отложить инициализацию объектов до первого вызова, и эта возможность есть и для коллекций. В арсенале Dagger 2 есть два способа для достижения отложенной инициализации: с использованием интерфейсов Provider<T> и Lazy<T>.

Lazy injections

Для любой зависимости T, вы можете применить Lazy<T>, данный способ позволяет отложить инициализацию до первого вызова Lazy<T>.get(). Если T синглтон, то будет возвращаться всегда один и тот же экземпляр. Если же T unscope, тогда зависимость T будет создана в момент вызова Lazy<T>.get и помещена в кэш внутри Lazy<T> и каждый последующий вызов именно этого Lazy<T>.get(), будет возвращать кэшированное значение.

Пример:

@Module
public class AppModule {
    @Singleton
    @Provides
    public GroupRepository groupRepository(Context context) {
        return new GroupRepositoryImpl(context);
    }

    @Provides
        return new UserRepositoryImpl(context);
    public UserRepository userRepository(Context context) {
    }
}

public class MainActivity extends AppCompatActivity {
    @Inject
    Lazy<GroupRepository> groupRepositoryInstance1;

    @Inject
    Lazy<GroupRepository> groupRepositoryInstance2;

    @Inject
    Lazy<UserRepository> userRepositoryInstance1;

    @Inject
    Lazy<UserRepository> userRepositoryInstance2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerAppComponent
                .builder()
                .context(this)
                .build()
                .inject(this);

        //GroupRepository @Singleton scope
        GroupRepository groupRepository1 = groupRepositoryInstance1.get();
        GroupRepository groupRepository2 = groupRepositoryInstance1.get();
        GroupRepository groupRepository3 = groupRepositoryInstance2.get();

        //UserRepository unscope
        UserRepository userRepository1 = userRepositoryInstance1.get();
        UserRepository userRepository2 = userRepositoryInstance1.get();
        UserRepository userRepository3 = userRepositoryInstance2.get();
    }
}

Инстансы groupRepository1, groupRepository2 и groupRepository3 будут равны, т.к. они имет скоуп синглтон.

Инстансы userRepository1 и userRepository2 будут равны, т.к. при первом обращении к userRepositoryInstance1.get() был создан объект и помещен в кэш внутри userRepositoryInstance1, а вот userRepository3 будет иметь другой инстанс, т.к. он имеет другой Lazy и для него был вызван первый раз get().

Provider injections

Provider<T> также позволяет отложить инициализацию объектов, но в отличии от Lazy<T>, значения unscope зависимостей не кэшируется в Provider<T> и возвращают каждый раз новый инстанс. Такой подход может понадобится к примеру когда у нас есть некая фабрика со скопом синглтон и эта фабрика должна предоставлять каждый раз новые объекты, рассмотрим пример:

@Module
public class AppModule {
   @Provides
   public Holder provideHolder() {
       return new Holder();
   }

   @Provides
   @Singleton
   public HolderFactory provideHolderFactory(Provider<Holder> holder) {
       return new HolderFactoryImpl(holder);
   }
}

public class HolderFactoryImpl implements HolderFactory {
    private Provider<Holder> holder;

    public HolderFactoryImpl(Provider<Holder> holder) {
        this.holder = holder;
    }

    public Holder create()  {
        return holder.get();
    }

}

public class MainActivity extends AppCompatActivity {
    @Inject
    HolderFactory holderFactory;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerAppComponent
                .builder()
                .context(this)
                .build()
                .inject(this);

        Holder holder1 = holderFactory.create();
        Holder holder2 = holderFactory.create();

    }
}

Здесь у нас holder1 и holder2 будут иметь разные инстансы, если бы мы использовали бы Lazy<T> вместо Provider<T> у нас бы эти объекты имели бы один инстанс из за кэширования.

Отложенную инициализацию можно применить и к Set:
Lazy<Set<T>> или Provider<Set<T>>, нельзя использовать так: Set<Lazy<T>>.

public class MainActivity extends AppCompatActivity {
    @Inject
    Lazy<Set<FileExporter>> fileExporters;
    //…
   // Set<FileExporter> exporters = fileExporters.get();
}

Map multibindings

Для того чтобы добавить элемент в Map, необходимо добавить аннотацию @IntoMap и аннотацию ключа (Наследники @MapKey) над @Provides методом в модуле:

@Module
public class ModuleA {

    @IntoMap
    @Provides
    @StringKey("xml")
    public FileExporter xmlFileExporter(Context context) {
        return new XmlFileExporter(context);
    }
}

@Module
public class ModuleB {

    @IntoMap
    @StringKey("csv")
    @Provides
    public FileExporter provideCSVFileExporter(Context context) {
        return new CSVFileExporter(context);
    }
}

@Component(modules = {ModuleA.class, ModuleB.class})
public interface AppComponent {
     //inject methods
}

Результат:

public class Values {
    @Inject
    public Values(Map<String, FileExporter> values) {
          //значения в values  {xml=XmlFileExporter,csv=CSVExporter}
    }
}

Также как и с Set, мы указали два наших модуля в компоненте, таким образом Dagger объединил наши значения в единую Map. Также можно использовать @Qualifier.

Стандартные типы ключей для Map:

  • IntKey
  • LongKey
  • StringKey
  • ClassKey

Стандартные типы ключей дополнительного модуля Dagger-Android:

  • ActivityKey
  • BroadcastReceiverKey
  • ContentProviderKey
  • FragmentKey
  • ServiceKey

Как выглядит реализация к примеру ActivityKey:

@MapKey
@Target(METHOD)
public @interface ActivityKey {
  Class<? extends Activity> value();
}

Можно создавать свои типы ключей, как выше описано или к примеру с enum:

public enum Exporters {
    XML,
    CSV
}

@MapKey
@Target(METHOD)
public @interface ExporterKey {
    Exporters value();
}

@Module
public class ModuleA {

    @IntoMap
    @Provides
    @ExporterKey(Exporters.XML)
    public FileExporter xmlFileExporter(Context context) {
        return new XmlFileExporter(context);
    }
}

@Module
public class ModuleB {

    @IntoMap
    @ExporterKey(Exporters.CSV)
    @Provides
    public FileExporter provideCSVFileExporter(Context context) {
        return new CSVFileExporter(context);
    }
}

public class Values {

    @Inject
    public Values(Map<Exporters, FileExporter> values) {
            //значения в values  {XML=XmlFileExporter,CSV=CSVExporter}
    }

}

Как и с Set мы можем применять отложенную инициализацию:
Lazy<Map<K,T>>, Provider<Map<K,T>>.

С Map мы можем использовать отложенную не только инициализацию самой коллекции, но инициализацию отдельного элемента и получать по ключу каждый раз новое значение (Map<K,Provider<T>>):

public class MainActivity extends AppCompatActivity {

    @Inject
    Map<Exporters, Provider<FileExporter>> exporterMap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerAppComponent
                .builder()
                .context(this)
                .build();

        FileExporter fileExporter1 = exporterMap.get(Exporters.CSV).get();
        FileExporter fileExporter2 = exporterMap.get(Exporters.CSV).get();

    }
}

fileExporter1 и fileExporter2 будут иметь разные инстансы. А элемент Exports.XML даже и не проинициализируется, т.к. мы к нему не обращались.

Мы не можем использовать Map<K, Lazy<T>>.

Чтобы запровайдить пустую коллекцию, нам необходимо добавить аннотацию @Multibinds над абстрактным методом:

@Module
public  abstract class AppModule {
    @Multibinds
    abstract Map<Exporters, FileExporter> exporters();
}

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

Subcomponents и мультибайндинг

Родительскому компоненту доступны коллекции указанные только в модулях родительского компонента, а сабкомпонент “наследует” все коллекции родительского компонента и объединяет их с коллекциями сабкомпонента:

@Module
public class AppModule {

    @IntoMap
    @Provides
    @ExporterKey(Exporters.XML)
    public FileExporter xmlFileExporter(Context context) {
        return new XmlFileExporter(context);
    }
}

@Module
public class ActivityModule {

    @IntoMap
    @ExporterKey(Exporters.CSV)
    @Provides
    public FileExporter provideCSVFileExporter(Context context) {
        return new CSVFileExporter(context);
    }
}

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
    ActivitySubComponent provideActivitySubComponent();

    //значения в коллекции {xml=XmlFileExporter}
    Map<Exporters, FileExporter> exporters(); 

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder context(Context context);

        AppComponent build();
    }

}

@ActivityScope
@Subcomponent(modules = {ActivityModule.class})
public interface ActivitySubComponent {
    //значения в коллекции  {XML=XmlFileExporter,CSV=CSVExporter}
    Map<Exporters, FileExporter> exporters(); 
}

@Binds + multibindings

Dagger 2 позволяет забайндить объекты в коллекцию с использованием абстрактных @Binds методов:

@Module
public abstract class LocationTrackerModule {

    @Binds
    @IntoSet
    public abstract LocationTracker netLocationTracker(NetworkLocationTracker
                tracker);

    @Binds
    @IntoSet
    public abstract LocationTracker fileLocationTracker(FileLocationTracker
               tracker);
}

Plugins

Для построения plugin-architected приложения, мы используем депенденси инджекшн фреймворк для того чтобы разделить интерфейсы от реализации, таким образом “Plugin” может быть повторно использован в различных приложениях:

Dagger 2 Multibindings - 1

С помощью Multibindings мы можем создать интерфейс и провайд метод, который будет являться точкой расширения для множества плагинов:

Dagger 2 Multibindings - 2

Вывод

По моему мнению Multibindings предоставляет достаточно широкие возможности для организации предоставления зависимостей, мы можем красиво организовать наши фабрики, а также подходит для реализации архитектуры расширения.

Пример на GitHub

Автор: txdrive

Источник

Поделиться

* - обязательные к заполнению поля