image


Сегодня хотелось бы поговорить о Dagger 2, в частности о dagger-android, Android Architecture Components, а так же о проблеме, с которой я столкнулся при их использовании. Наверное, пост не столько познавательный, сколько философский, сразу прошу не кидаться тапками, потому как причины создания поста есть (как минимум субъективные), о них расскажу под катом.


Долго размышлял, стоит ли писать этот пост, но внутреннее равновесие пошатнулось, а значит посту быть.


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


Теперь о том, чего я хотел добиться с помощью этих инструментов:


  • Забыть о кошмаре со сменой конфигурации при помощи «живучих» вьюмоделей
  • Использовать Dagger для поставки зависимостей прямо во вьюмодель
  • Организовать Scopes для зависимостей
  • Использовать новомодный dagger-android

Теперь немного кода для лучшего понимания:


Application
class App : DaggerApplication() {

    override fun applicationInjector(): AndroidInjector<out App> = DaggerAppComponent.builder().create(this)

}

AppComponent
@Singleton
@Component(modules =
[
    (AndroidSupportInjectionModule::class),
    (AppModule::class),
    (ActivityBuilder::class)
])
interface AppComponent : AndroidInjector<App> {
    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<App>()
}

ActivityBuilder
@Module
abstract class ActivityBuilder {

    @ActivityScope
    @ContributesAndroidInjector(modules = [MainActivityModule::class, FragmentProvider::class, RepositoryModule::class])
    abstract fun bindMainActivity(): MainActivity
}

RepositoryModule
@Module
class RepositoryModule {

    @Provides
    @ActivityScope
    fun provideSomeDataRepository(socket: SocketService, restApi: RestApi, feedDao: FeedDao, userSettings: UserSettings): SomeDataRepository
            = SomeDataRepositoryImpl(socket, restApi, feedDao, userSettings)

Инъекция ViewModelFactory
    @Inject
    lateinit var factory: MainViewModelFactory

    private lateinit var viewModel: MainViewModel

    viewModel = ViewModelProviders.of(this, factory)
    .get(MainViewModel::class.java)

ViewModelFactory
class MainViewModelFactory @Inject constructor(private val someDataRepository: SomeDataRepository) : ViewModelProvider.Factory{

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModelImpl(someDataRepository) as T
        }
        throw IllegalArgumentException(String.format("%s Not Found", modelClass.simpleName))
    }
}

ViewModel
class MainViewModelImpl @Inject constructor(someDataRepository: SomeDataRepository) : MainViewModel(), MainViewModel.Input, MainViewModel.Output

Так вот, насчет того списка, чего я хотел получить от всех этих манипуляций.


Всё хорошо, всё работает, только вот я заметил одну особенность (чуть позже она стала очевидной): После смены конфигурации Dagger создает зависимости заново.Если поставить breakpoint на init блок в SomeDataRepositoryImpl, то мы увидим, что при добавлении фрагмента с такой же зависимостью — во фрагмент передается экземпляр того же объекта, что был создан при создании активности (breakpoint не сработал). Но при перевороте экрана, мы будем наблюдать создание нового объекта (breakpoint сработал), который не дойдет до вьюмодели, потому как она уже имеет экземпляр этого объекта.


Из за небольшого опыта разработки с этими инструментами, было принято решение — спросить совета у других разработчиков, на что я получал ответы, что эта ситуация — норма, что сборщик мусора справится с этим бардаком, и всё будет нормально работать.


Отсылаясь к заголовку поста, про "невпихиваемое", это было вычитано где-то в issues по dagger (не цитата слово в слово, но смысл такой же): "Вы пытаетесь вобрать всё лучшее из этих двух инструментов, не думаю, что это возможно"


Спойлер

Конечно, можно не использовать даггер-андроид, и подавать зависимости прямо во вьюмодель, используя попутно AndroidViewModel(application: Applitaction), но так не пойдет, ведь вьюмодель не должна знать о классе Application. Для меня до сих пор загадка — зачем этот класс вообще существует. Может кто-то объяснит в комментариях?


Подводя итог ко всему выше сказанному, у меня один вопрос: Можно ли сделать то, что я хочу? Можно ли избавиться от этого неконтролируемого создания объектов даггера при смене конфигурации?


Буду рад ответам в комментариях, спасибо за внимание.

Комментарии (15)


  1. nickchickin
    27.02.2018 13:43

    MainViewModelFactory умирает при пересоздании активити, а так как она не является синглтоном, даггер создает новый обьект, подтягивает зависимость на репозиторий. Репозиторий помечен при помощи @ActivityScope, поэтому он не пересоздается, по идее если фабрике тоже дать скоуп, то и фабрика не будет пересоздаваться


    1. Vertoletique Автор
      27.02.2018 13:44

      Я это понимаю не так.
      Если пометить фабрику скоупом, то тогда она обязательно пересоздастся, потому как объекты в скоупе живут пока не умрет Activity


      1. nickchickin
        27.02.2018 13:52

        Правильно, но если ее пометить скоупом который лежит на уровень ниже, например если нам фабрика понадобиться на наскольких активити, то она не умрeт. Например ApplicationScope


  1. nickchickin
    27.02.2018 13:52

    -


    1. Vertoletique Автор
      27.02.2018 14:03

      Тогда получим конфликт, потому как нельзя внутри сабкомпонента аннотировать provide-методы более высоким скоупом


      1. nickchickin
        27.02.2018 14:19

        Следовательно, необходимо вынести Фабрику в AppModule и сделать ее синглтоном. И эта фабрика будет создавать все ViewModel для приложения. Я тоже столкнулся с такой проблемой, поскольку для каждого экрана делал свою фабрику, в нее включал зависимости, необходимые для ViewModel. Поэтому теперь применяю подход как в примере гугла github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java


        1. Vertoletique Автор
          27.02.2018 14:23

          Попробую этот вариант, спасибо за ссылку
          Но почему-то мне кажется, что в таком случае фабрика не имеет доступ к модулям сабкомпонента, а значит все зависимости по этой логике будут @.Singleton


          1. Logos_Intellect
            01.03.2018 13:35

            Тоже у себя использую пример Google, но на Kotlin.
            gist.github.com/LogosIntellect/f786c218bea72cbfee69099d3d163ea2


            1. Vertoletique Автор
              01.03.2018 13:36

              Спасибо за сылку, ознакомлюсь


  1. deej
    01.03.2018 13:35

    Я правильно понимаю, что при смене конфигурации пересоздаются Dagger-зависимости, но ViewModel остается прежней?


    1. Vertoletique Автор
      01.03.2018 13:35

      Правильно


      1. deej
        01.03.2018 15:00

        Правильным решением было бы предотвращать пересоздание Activity-компонента при смене конфигурации (как — отдельный вопрос, есть несколько способов), но с dagger-android, насколько я понял, разработчик не контролирует время жизни компонента. Тогда остается либо вносить изменения в сам dagger-android, либо отказаться от него.


        1. Vertoletique Автор
          01.03.2018 15:26

          А можно подробнее про «несколько способов»?


          1. deej
            01.03.2018 15:37

            1. Хранить компонент в retained headless fragment
            2. Сохранять компонент в saved state. Сериализация не нужна, если обернуть объект в Binder. Подробнее техника описана тут https://habrahabr.ru/post/274635/. Всё, что есть в статье не нужно, достаточно ValueBinder + BundleCompat.


            1. Vertoletique Автор
              01.03.2018 15:44

              Спасибо за инфу, лайкнул бы, да кармы мало