Kodein. Основы

    Не нашел понятных гайдов для тех, кто Kodein видит в первый раз, а документация не во всех местах прозрачная и последовательная, поэтому хочу поделиться основными возможностями библиотеки с вами. Некоторые возможности библиотеки будут выпущены, но это в основном advanced часть. Здесь же вы найдете всё, чтобы по ходу чтения статьи нормально стартовать и начать внедрять зависимости с Kodein. Статья базируется на Kodein 5.3.0, так как Kodein 6.0.0 требует Support Library 28 или AndroidX и далеко не скоро все перейдут на них, так как многие сторонние библиотеки ещё не предлагают совместимых версий.


    Kodein — это библиотека для реализации внедрения зависимостей (DI). Если вы не знакомы с этим понятием, то прочтите начало статьи о Dagger2, где автор кратко объясняет теоретические аспекты DI.

    В этой статье мы будем рассматривать всё на примере Android, но, по заявлению разработчиков, Kodein ведет себя одинаково на всех платформах, которые поддерживаются Kotlin (JVM, Android, JS, Native).

    Установка


    В связи с тем, что в Java есть type erasure, возникает проблема — компилятор стирает обобщенный тип. На уровне байткода List<String> и List<Date> — это просто List. Все же остается путь получить информацию об обобщенных типах, но стоить это будет дорого, а работать только на JVM и Android. В связи с этим разработчики Kodein предлагают использовать одну из двух зависимостей: одна при работе получает информацию об обобщенных типах (kodein-generic), а другая нет(kodein-erased). Если на примере, то при использовании kodein-erased List<String> и List<Date> будут сохранены как List<*>, а при использовании kodein-generic всё сохранится вместе с указанным типом, то есть как List<String> и List<Date> соответственно.

    Как выбрать?

    Пишите не под JVM — используйте kodein-erased, по другому нельзя.
    Пишите под JVM и вопрос производительности для вас уж очень важен — можете использовать kodein-erased, но будьте осторожны, этот опыт может оказаться неожиданным в плохом смысле этих слов. Если же вы создаете обычное приложение без особых требований к производительности — используйте kodein-generic.

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

    Итак, устанавливаем:

    Первое — в build.gradle среди репозиториев должен быть jcenter(), если его там нет — добавьте.

    buildscript {
        repositories {
            jcenter()
        }
    }
    

    Далее, в блок dependencies добавляем одну из базовых зависимостей, о которых говорили выше:

    implementation "org.kodein.di:kodein-di-generic-jvm:$version"
    

    implementation "org.kodein.di:kodein-di-erased-jvm:$version"
    

    Так как мы говорим об Android — зависимостей станет больше. Можно конечно, обойтись и без этого, Kodein будет нормально функционировать, но зачем отказываться от дополнительных, полезных для Android фич (о них я расскажу в конце статьи)? Выбор за вами, но предлагаю добавить.

    Здесь также есть опции.

    Первая — вы не используете SupportLibrary

    implementation "org.kodein.di:kodein-di-framework-android-core:$version"
    

    Вторая — используете

    implementation "org.kodein.di:kodein-di-framework-android-support:$version"
    

    Третья — вы используете AndroidX

    implementation "org.kodein.di:kodein-di-framework-android-x:$version"
    

    Начинаем создавать зависимости


    Используя Dagger2, я привык создавать и инициализировать зависимости при старте приложения, в классе Application.

    С Kodein это делается так:

    class MyApp : Application() {
    	val kodein = Kodein { 
    	    /* Зависимости */
    	}
    }
    

    Объявление зависимостей всегда начинается с

    bind<TYPE>() with
    

    Теги


    Тегирование зависимостей в Kodein — это фича, схожая по функционалу с Qualifier из Dagger2. В Dagger2 вам нужно делать или отдельные Qualifier или использовать @Named("someTag"), который по факту тоже Qualifier. Суть проста — таким способом вы различите две зависимости одного типа. Например, вам нужно получать Сontext приложения или конкретной Activity в зависимости от ситуации, следовательно вам нужно при объявлении зависимостей указать теги для этого. Kodein позволяет объявить одну зависимость без тега, она будет базовой и если при получении зависимости тег не указывать, то получим именно её, остальные же нужно пометить тегом и при получении зависимости тег нужно будет указать.

    val kodein = Kodein {
        bind<Context>() with ... 
        bind<Context>(tag = "main_activity") with ... 
        bind<Context>(tag = "sale_activity") with ... 
    }
    

    Параметр tag имеет тип Any, поэтому можно использовать не только строки. Но помните о том, что классы, используемые как теги, должны реализовывать методы equals и hashCode. Передавать тег в функцию всегда нужно как именованный аргумент независимо от того создаете вы зависимость или получаете.

    Типы внедрения зависимости


    В Kodein есть несколько способов предоставлять зависимости, начнем с самого необходимого — создания синглтонов. Жить синглтон будет в рамках созданного экземпляра Kodein.

    Внедряем синглтоны


    Начнем с примера:

    val kodein = Kodein {
        bind<IMyDatabase>() with singleton { RoomDb() } 
    }
    

    Таким образом мы предоставляем (провайдим) IMyDatabase, за котором будет спрятан экземпляр RoomDb. Создан экземпляр RoomDb будет при первом запросе зависимости, повторно создаваться он не будет, пока не будет создан новый экземпляр Kodein. Синглтон создается синхронизированным, но при желании можно сделать и несинхронизированный. Это увеличит производительность, но вы должны понимать риски, которые за этим следуют.

    val kodein = Kodein {
        bind<IMyDatabase>() with singleton(sync = false) { RoomDb() } 
    }
    

    Если есть необходимость создавать экземпляр зависимости не при первом вызове, а сразу после создания экземпляра Kodein — используйте другую функцию:

    val kodein = Kodein {
        bind<IMyDatabase>() with eagerSingleton { RoomDb() } 
    }
    

    Постоянно создаем новый экземпляр зависимости


    Есть возможность создавать не синглтоны, а постоянно при обращении к зависимости получать новый её экземпляр. Для этого используется функция provider:

    val kodein = Kodein {
        bind<IMainPresenter>() with provider { QuantityPresenter() } 
    }
    

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

    Постоянно создаем новый экземпляр зависимости и передаем параметры в конструктор зависимости


    Вы можете при каждом запросе зависимости получать новый её экземпляр, как и в предыдущем примере, но при этом указывать параметры для создания зависимости. Параметров может быть максимум 5. Для подобного поведения используем метод factory.

    val kodein = Kodein {
        bind<IColorPicker>() with factory { r: Int, g: Int, b: Int, a: Int -> RgbColorPicker(r, g, b, a) } 
    }
    

    Каждый раз создаем кэшированный экземпляр в зависимости от параметров


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

    val kodein = Kodein {
        bind<IRandomIntGenerator>() with multiton { from: Int, to: Int -> IntRandom(from, to) } 
    }
    

    В приведенном примере при первом получении зависимости с параметрами 5 и 10 мы создадим новый экземпляр IntRandom(5, 10), при повторном же вызове зависимости с теми же параметрами мы получим ранее созданный экземпляр. Таким образом получается некий map из синглтонов с ленивой инициализацией. Аргументов, как и в случае с factory максимум 5.

    Как и в случае с синглтонами здесь можно отключить синхронизацию.

    val kodein = Kodein {
        bind<IRandomIntGenerator>() with multiton(sync = false) { from: Int, to: Int -> IntRandom(from, to) } 
    }
    

    Использование Soft и Weak ссылок в Kodein


    При предоставлении зависимостей с помощью singleton или multiton вы можете указать тип ссылки на хранимый экземпляр. В обычно случае, что мы рассматривали выше — это будет обычная strong ссылка. Но есть возможность использовать soft и weak ссылки. Если вы плохо знакомы с этими понятиями, то загляните сюда.

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

    val kodein = Kodein {
        bind<IMyMap>() with singleton(ref = softReference) { WorldMap() } 
        bind<IClient>() with singleton(ref = weakReference) { id -> clientFromDB(id) } 
    }
    

    Отдельный синглтон для каждого потока


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

    val kodein = Kodein {
        bind<Cache>() with singleton(ref = threadLocal) { LRUCache(16 * 1024) } 
    }
    

    Константы, как внедряемые зависимости


    Можно и константы провайдить как зависимости. В документации обращается внимание на то, что вы должны провайдить с помощью Kodein константы простых типов без наследования или интерфейсов, к примеру примитивы или data классы.

    val kodein = Kodein {
        constant(tag = "maxThread") with 8 
        constant(tag = "serverURL") with "https://my.server.url"
    

    Создаем зависимости без изменения типа


    Например, вы хотите предоставить зависимость как синглтон, но при этом не прятать её за интерфейс. Можно просто не указывать тип при вызове bind и вместо with использовать from.

    val kodein = Kodein {
        bind() from singleton { Gson() }
    

    Зависимость в примере выше будет иметь тип возвращаемого значения функции, то есть предоставляться будет зависимость типа Gson.

    Создаем зависимости по подтипу суперкласса или интерфейса


    Kodein позволяет предоставлять зависимость по-разному для наследников определенного класса или же классов, реализующих один интерфейс.

    val kodein = Kodein {
        bind<Animal>().subTypes() with { animalType ->
                    when (animalType.jvmType) {
                        Dog::class.java -> eagerSingleton { Dog() }
                        else ->  provider { WildAnimal(animalType) }
                    }
                }
    

    Класс Animal может быть как суперклассом, так и интерфейсом, используя .subtypes мы получаем animalType с типом TypeToken<*>, из которого мы уже можем получить Java класс и, в зависимости от него, по разному предоставлять зависимость. Эта фича может быть полезна, если вы используете TypeToken или его производные в качестве параметра конструктора для ряда случаев. Также этим способом можно избежать лишнего кода с одинаковым созданием зависимостей для разных типов.

    Создаем зависимости, которым в качестве параметров нужны другие зависимости


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

    class ProductGateway(private val api: IProductApi, 
                         private val dispatchers: IDispatchersContainer) : IProductGateway 
    

    Для того, чтобы создать класс с зависимостями, которые ранее были созданы в Kodein достаточно в качестве параметров передать вызов функции instance(). При этом порядок создания не важен.

    bind<IDispatchersContainer>() with singleton { DispatchersContainer() }
    bind<IProductGateway>() with singleton { ProductGateway(instance(), instance()) }
    bind<IProductApi>() with singleton { ProductApi() }
    

    Вместо instance() здесь могут быть вызовы provider() или factory(), подробнее познакомимся с этими методами в разделе получения и внедрения зависимостей.

    Создаем зависимость путем вызова метода ранее созданной зависимости


    Звучит не очень, но можно вызовом instance<TYPE> получить класс, который мы уже где-то провайдим и вызовом метода этого класса получить новую зависимость.

    bind<DataSource>() with singleton { MySQLDataSource() }
    bind<Connection>() with provider { instance<DataSource>().openConnection() }
    

    Модули


    Используя Dagger2, я привык разделать зависимости по модулям. В Kodein, на первый взгляд, выглядит всё не очень. Вам нужно прямо в Application классе создавать множество зависимостей и лично мне это не очень нравится. Но выход есть, Kodein тоже позволяет создавать модули, а потом подключать их в тех местах, где необходимо.

        val appModule = Kodein.Module("app") {
            bind<Gson>() with singleton { provideGson() }
            bind<HttpClient>() with singleton { provideHttpClient() }
        }
    
        val kodein: Kodein = Kodein {
                import(appModule)
                bind<ISchedulersContainer>() with singleton { SchedulersContainer() }
                // другие зависимости
        }
    

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

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

    import(apiModule.copy(name = "firstAPI"))
    import(secondApiModule.copy(prefix = "secondAPI-"))
    

    Мне привычно работать, когда модули зависят друг от друга и составляют какую-либо иерархию. В Kodein можно импортировать каждый модуль единожды, поэтому, если в один объект Kodein вы попытаетесь импортировать два модуля, у которых есть одинаковые зависимые модули, то приложение упадет. Выход простой — нужно использовать для импорта вызов importOnce(someModule), который проверит не был ли ранее импортирован модуль с аналогичным именем, а после при необходимости импортирует.

    Например в таких случаях приложение упадет:

        val appModule = Kodein.Module("app") {
            bind<Gson>() with singleton { provideGson() }
        }
    
        val secondModule = Kodein.Module("second") {
            import(appModule)
        }
    
        val thirdModule = Kodein.Module("third") {
            import(appModule)
        }
    
        val kodein: Kodein = Kodein {
                import(secondModule)
                import(thirdModule)
            }
    

        val appModule = Kodein.Module("app") {
            bind<Gson>() with singleton { provideGson() }
        }
    
        val secondModule = Kodein.Module("second") {
            importOnce(appModule)
        }
    
        val thirdModule = Kodein.Module("third") {
            import(appModule)
        }
    
        val kodein: Kodein = Kodein {
                import(secondModule)
                import(thirdModule)
            }
    

    Но если вызов importOnce будет при повторной попытке подключения, то все заработает. Будьте внимательны.

        val appModule = Kodein.Module("app") {
            bind<Gson>() with singleton { provideGson() }
        }
    
        val secondModule = Kodein.Module("second") {
            import(appModule)
        }
    
        val thirdModule = Kodein.Module("third") {
            importOnce(appModule)
        }
    
        val kodein: Kodein = Kodein {
                import(secondModule)
                import(thirdModule)
            }
    

    Наследование


    Если использовать один модуль дважды, то будут созданы разные зависимости, но как же быть с наследованием и реализовать поведение, подобное Subcomponents в Dagger2? Все просто, нужно лишь унаследоваться от экземпляра Kodein и вы получите в наследнике доступ ко всем зависимостям родителя.

    val kodein: Kodein = Kodein {
                bind<ISchedulersContainer>() with singleton { SchedulersContainer() }
                // другие зависимости
        }
    
    val subKodein = Kodein {
            extend(kodein)
            // новые зависимости
        }
    

    Переопределение


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

    val kodein = Kodein {
        bind<Api>() with singleton { ApiImpl() }
        /* ... */
        bind<Api>(overrides = true) with singleton { OtherApiImpl() }
    }
    

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

    Звучит не совсем понятно, давайте на примерах. В этих случаях приложение упадет:

        val appModule = Kodein.Module("app") {
            bind<Gson>() with singleton { provideGson() }
        }
    
        val kodein: Kodein = Kodein {
            bind<Gson>() with singleton { provideGson() }
            import(appModule)
        }
    

        val appModule = Kodein.Module("app") {
            bind<Gson>() with singleton { provideGson() }
        }
    
        val kodein: Kodein = Kodein {
            bind<Gson>() with singleton { provideGson() }
            import(appModule, allowOverride = true)
        }
    

    А в этом зависимость модуля перезапишет зависимость, объявленную в объекте Kodein.

        val appModule = Kodein.Module("app") {
            bind<Gson>(overrides = true) with singleton { provideGson() }
        }
    
        val kodein: Kodein = Kodein {
            bind<Gson>() with singleton { provideGson() }
            import(appModule, allowOverride = true)
        }
    

    Но если очень хочется и вы понимаете что делаете, то можно создать такой модуль, который при наличии одинаковых зависимостей с объектом Kodein будет их переопределять и приложение при этом не упадет. Используем у модуля параметр allowSilentOverride.

    val testModule = Kodein.Module(name = "test", allowSilentOverride = true) {
        bind<EmailClient>() with singleton { MockEmailClient() } 
    }
    

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

    Извлекаем и внедряем зависимости


    Наконец мы разобрались как объявлять зависимости множеством способов, пора разобраться как же их получить в своих классах.

    Разработчики Kodein разделяют два способа получения зависимостей — injection и retieval. Если коротко, то injection — это когда класс получает все зависимости при создании, то есть в конструктор, а retrieval — это когда класс сам отвечает за получение своих зависимостей.

    При использовании injection ваш класс ничего не знает о Kodein и код в классе получается чище, но если использовать retrieval, то у вас появляется возможность гибче управлять зависимостями. В случае с retrieval все зависимости получаются лениво, только в момент первого обращения к зависимости.

    Методы Kodein, с помощью которых можно получить зависимости


    У экземпляра класса Kodein есть три метода, которые вернут вам зависимость, фабрику зависимостей или провайдер зависимостей — это instance(), factory() и provider() соответственно. Таким образом, если вы предоставляете зависимость с помощью factory или provider, то и получать вы можете не только результат выполнения функции, но и саму функцию. Не забывайте, что во всех вариантах можно использовать теги.

        val kodein: Kodein = Kodein {
            bind<BigDecimal>() with factory { value: String -> BigDecimal(value) }
            bind<Random>() with provider { Random() }
        }
    
        private val number: BigDecimal by instance(arg = "23.87")
        private val numberFactory: (value: String) -> BigDecimal by factory()
    
        private val random: Random by instance()
        private val randomProvider: () -> Random by provider()
    

    Внедрение зависимостей через конструктор


    Как вы уже поняли, речь пойдет именно об injection. Для реализации нужно сначала вынести все зависимости класса в его конструктор, а после создать экземпляр класса с помощью вызова kodein.newInstance

    class ProductApi(private val client: HttpClient, private val gson: Gson) : IProductApi
    
    class Application : Application() {
    
        val kodein: Kodein = Kodein {
            bind<Gson>() with singleton { provideGson() }
            bind<HttpClient>() with singleton { provideHttpClient() }
        }
    
        private val productApi: IProductApi by kodein.newInstance { ProductApi(instance(), instance()) }
    }
    

    Внедрение зависимостей в nullable свойства


    Вполне может возникнуть ситуация, когда вы не знаете была ли объявлена зависимость. Если зависимость не объявлена в экземпляре Kodein, то код из примера выше приведет к Kodein.NotFoundException. Если же вы хотите получить как результат функции null если зависимости нет, то для этого есть три вспомогательные функции: instanceOrNull(), factoryOrNull() и providerOrNull().

    class ProductApi(private val client: HttpClient?, private val gson: Gson) : IProductApi
    
    class Application : Application() {
    
        val kodein: Kodein = Kodein {
            bind<Gson>() with singleton { provideGson() }
        }
    
        private val productApi: IProductApi by kodein.newInstance { ProductApi(instanceOrNull(), instance()) }
    }
    

    Получаем зависимости внутри класса


    Как уже упоминалось, в случае, когда используем retrieval — инициализация всех зависимостей по умолчанию ленивая. Это позволяет получать зависимости только в тот момент, когда они нужны, и получать зависимости в классах, которые создает система.

    Activity, Fragment и другие классы с собственным жизненным циклом, это всё о них.

    Для внедрения зависимостей в Activity нам нужна лишь ссылка на экземпляр Kodein, после чего мы можем воспользоваться уже известными методами. На самом деле выше вы уже видели примеры retrieval, нужно лишь объявить свойство и делегировать его одной из функций: instance(), factory() или provider()

    private val number: BigDecimal by kodein.instance(arg = "23.87")
    private val numberFactory: (value: String) -> BigDecimal by kodein.factory()
    private val random: Random? by kodein.instanceOrNull()
    private val randomProvider: (() -> Random)? by kodein.providerOrNull()
    

    Передача параметров в фабрики


    Выше вы уже увидели, что для того, чтобы передать параметр в фабрику достаточно использовать параметр arg функции instance. Но что делать, если параметров несколько (ранее я говорил о том, что параметров в фабрике может быть до 5)? Нужно лишь передать в параметр arg класс M, который имеет перегруженные конструкторы и может принимать от 2 до 5 аргументов.

    val kodein = Kodein {
        bind<IColorPicker>() with factory { r: Int, g: Int, b: Int, a: Int -> RgbColorPicker(r, g, b, a) } 
    }
    
    val picker: IColorPicker by kodein.instance(arg = M(255, 211, 175, 215))
    

    Принудительная инициализация зависимостей


    Как говорилось — по умолчанию инициализация ленивая, но можно создать триггер, привязать его к свойству, нескольким свойствам или целому экземпляру Kodein, после дернуть этот триггер и зависимости будут инициализированы.

    val myTrigger = KodeinTrigger()
    val gson: Gson by kodein.on(trigger = myTrigger).instance()
    /*...*/
    myTrigger.trigger() // Здесь произойдет инициализация экземпляра Gson
    

    val myTrigger = KodeinTrigger()
    val kodeinWithTrigger = kodein.on(trigger = myTrigger)
    val gson: Gson by kodeinWithTrigger.instance()
    /*...*/
    myTrigger.trigger() // Здесь произойдет инициализация всех требуемых зависимостей из kodeinWithTrigger
    

    Ленивое создание экземпляра Kodein


    До этого мы постоянно явно создавали экземпляр Kodein, но есть возможность отложить инициализацию этого свойства с помощью класса LazyKodein, который принимает в конструктор функцию, которая должна вернуть объект Kodein.

    Такой подход может быть полезен, если, например, неизвестно, понадобятся ли вообще зависимости из данного экземпляра Kodein.

    val kodein: Kodein = LazyKodein {
        Kodein {
            bind<BigDecimal>() with factory { value: String -> BigDecimal(value) }
            bind<Random>() with provider { Random() }
        }
    }
    
    private val number: BigDecimal by kodein.instance(arg = "13.4")
    /* ... */
    number.toPlainString() // Здесь произойдет инициализация объекта kodein и требуемой зависимости
    

    К аналогичному результату приведет вызов Kodein.lazy

        val kodein: Kodein = Kodein.lazy {
            bind<BigDecimal>() with factory { value: String -> BigDecimal(value) }
            bind<Random>() with provider { Random() }
        }
    
        private val number: BigDecimal by kodein.instance(arg = "13.4")
        /* ... */
        number.toPlainString() // Здесь произойдет инициализация объекта kodein и требуемой зависимости
    

    Отложенная инициализация Kodein


    Для отложенной инициализации Kodein существует объект LateInitKodein. Вы можете создать этот объект, делегировать ему создание свойств, а уже после инициализировать сам объект, задав ему свойство baseKodein, после чего уже можно обращаться к зависимостям.

    val kodein = LateInitKodein()
    val gson: Gson by kodein.instance()
    /*...*/
    kodein.baseKodein = /* передаем ссылку на экземпляр Kodein */ 
    /*...*/
    gson.fromJson(someStr)
    

    Получаем все экземпляры указанного типа


    Можно попросить у Kodein экземпляр указанного типа и всех его наследников в виде List. Всё только в рамках указанного тега. Для этого есть методы allInstances, allProviders, allFactories.

        val kodein: Kodein = Kodein {
            bind<Number>() with singleton { Short.MAX_VALUE }
            bind<Double>() with singleton { 12.46 }
            bind<Double>("someTag") with singleton { 43.89 }
            bind<Int>() with singleton { 4562 }
            bind<Float>() with singleton { 136.88f }
        }
        
        val numbers: List<Number> by kodein.allInstances()
    

    Если выведете в лог, то увидите там [32767, 136.88, 4562, 12.46]. Зависимость с тегом в список не попала.

    Упрощаем получение зависимостей с помощью интерфейса KodeinAware


    Этот интерфейс обязывает вас переопределить свойство типа Kodein, а взамен предоставляет доступ ко всем функциям, доступным экземпляру Kodein.

    class MyApplication : Application(), KodeinAware {
    
        override val kodein: Kodein = Kodein {
            bind<Number>() with singleton { Short.MAX_VALUE }
            bind<Double>() with singleton { 12.46 }
            bind<Double>("someTag") with singleton { 43.89 }
            bind<Int>() with singleton { 4562 }
            bind<Float>() with singleton { 136.88f }
        }
    
        val numbers: List<Number> by allInstances()
    }
    

    Как видно, теперь можно просто написать by allInstances() вместо by kodein.allInstances()

    Ранее мы уже говорили о триггере для получения зависимостей. В интерфейсе KodeinAware вы можете переопределить триггер и получать все объявленные зависимости по вызову этого триггера.

    class MyApplication : Application(), KodeinAware {
    
        override val kodein: Kodein = Kodein {
            bind<Number>() with singleton { Short.MAX_VALUE }
            bind<Double>() with singleton { 12.46 }
            bind<Double>("someTag") with singleton { 43.89 }
            bind<Int>() with singleton { 4562 }
            bind<Float>() with singleton { 136.88f }
        }
        override val kodeinTrigger = KodeinTrigger()
    
        val numbers: List<Number> by allInstances()
    
        override fun onCreate() {
            super.onCreate()
            kodeinTrigger.trigger()
        }
    }
    

    Так как доступ к зависимостям и экземпляру Kodein осуществляется лениво — можно делегировать инициализацию экземпляра Kodein встроенной в Kotlin функции lazy. Такой подход может быть полезен в классах, зависящих от своего контекста, например, в Activity.

    class CategoriesActivity : Activity(), KodeinAware {
        override val kodein: Kodein by lazy { (application as MyApplication).kodein }
        private val myFloat: Float by instance()
    

    По тем же причинам можно использовать модификатор lateinit.

    class CategoriesActivity : Activity(), KodeinAware {
        override lateinit var kodein: Kodein 
        private val myFloat: Float by instance()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            kodein = (application as MyApplication).kodein
        }
    

    Доступ к зависимостям без делегирования свойств


    Если по какой-то причине вы не хотите использовать делегирование свойств, то можно использовать прямой доступ через DKodein (от direct). Основное отличие будет в том, что ленивой инициализации уже не будет, зависимость будет получаться сразу в момент вызова instance, provider и подобных функций. Получить DKodein можно из имеющегося экземпляра Kodein или же собрать с нуля.

    class MyApplication : Application(), KodeinAware {
    
        override val kodein: Kodein = Kodein {
            bind<BigDecimal>() with singleton { BigDecimal.TEN }
        }
    
        val directKodein: DKodein = kodein.direct
    
        val directKodein2: DKodein = Kodein.direct {
            bind<BigDecimal>() with singleton { BigDecimal.ONE }
        }
    
        val someNumber:BigDecimal = directKodein.instance()
        val someNumber2:BigDecimal = directKodein2.instance()
    

    Kodein можно использовать в рамках KodeinAware, а DKodein в рамках DKodeinAware, можете поэкспериментировать.

    Получаем зависимости в рамках какого-либо контекста


    Для того, чтобы получить из одного объекта Kodein несколько зависимостей одного типа мы уже разобрали вариант использования тегов и фабрики с аргументами, но есть ещё один — использовать контекст (и это не тот контекст, который в Android).

    Отличия от зависимости с тегом:

    • Тег нельзя использовать внутри функции, в которой мы создаем зависимость
    • При использовании контекста мы имеем доступ к экземпляру контекста в функции создания зависимости

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

    Например, у вас есть Activity и Presenter, а вы хотите, используя один объект Kodein, предоставлять несколько зависимостей разных типов по разному, в зависимости от того, в каком классе они получаются. Чтобы привести Activity и Presenter к одному типу — вам понадобится дополнительный интерфейс, а в фабрике придется проверять тип полученного аргумента. Схема не очень удобная. Поэтому смотрим как воспользоваться контекстом:

    class MyApplication : Application(), KodeinAware {
    
        override val kodein: Kodein = Kodein {
            bind<BigDecimal>() with contexted<CategoriesActivity>().provider { context.getActivityBigDecimal() }
            bind<BigDecimal>() with contexted<CategoriesPresenter>().factory { initialValue:BigDecimal -> context.getPresenterBigDecimal(initialValue) }
        }
    }
    
    class CategoriesActivity : Activity(),  AppKodeinAware {
    
        fun getActivityBigDecimal() = BigDecimal("16.34")
    
        private val activityBigDecimal: BigDecimal by kodein.on(context = this).instance()
    }
    
    class CategoriesPresenter : AppKodeinAware {
    
        fun getPresenterBigDecimal(initialValue: BigDecimal) = initialValue * BigDecimal.TEN
    
        private val presenterBigDecimal: BigDecimal by kodein.on(context = this).instance(arg = BigDecimal("31.74"))
    }
    

    Пример, конечно, за уши притянут и в реальной практике вряд ли у вас возникнет именно такая ситуация, но на этом примере видно как работает тот самый контекст.

    Чтобы объявить зависимость вы указываете не with provider(), а with contexted<OurContextClass>().provider, где OurContextClass — это тип класса, экземпляр которого будет выступать в роли контекста. contexted может быть только provider или factory.

    Доступ к этому контексту в функции, которая возвращает зависимость, осуществляется через переменную под именем context.

    Чтобы получить зависимость привязанную к контексту — вам сначала нужно указать контекст объекту Kodein через функцию on(), а после уже запрашивать зависимость.

    Аналогично контекст используется в случае с injection.

    private val productApi: IProductApi by kodein.on(context = someContext).newInstance { ProductApi(instance(), instance()) }
    }
    

    Расширения для Android


    В начале статьи я обещал рассмотреть и возможности расширения для Android.
    Ничто не мешает вам использовать Kodein так как мы рассматривали выше, но можно сделать всё на порядок удобнее.

    Встроенный модуль Kodein для Android


    Очень полезная вещь — модуль, подготовленный для Android. Для его подключения необходимо, чтобы класс Application реализовывал KodeinAware и инициализировал свойство Kodein лениво (для доступа к экземпляру Application). Взамен вы получаете огромное количество объявленных зависимостей, которые можно получить из класса Application, включая всем необходимый Context. Как подключить — смотрим на пример.

    class MyApplication : Application(), KodeinAware {
        override val kodein = Kodein.lazy {
            import(androidModule(this@MyApplication))
    	    // зависимости
        }
    
        val inflater: LayoutInflater by instance() 
    }
    

    Как видно — можно получить, например, LayoutInflater. Для ознакомления с полным списком объявленных в модуле зависимостей — смотрим сюда.

    Если хотите получить эти зависимости вне Android классов, знающих о своем контексте — укажите контекст явно.

    val inflater: LayoutInflater by kodein.on(context = getActivity()).instance()
    

    Быстро получаем родительский Kodein через closestKodein()


    Всё просто, в Android одни объекты зависят от других. На верхнем уровне есть Application, под которым Activity, далее Fragment. Вы можете в Activity реализовать KodeinAware, а в качестве инициализации вызвать closestKodein() и таким образом получить экземпляр Kodein из Application.

    class MyActivity : Activity(), KodeinAware {
        override val kodein by closestKodein()
        val ds: DataSource by instance()
    }
    

    closestKodein можно также получить вне Android классов, но необходим контекст Android, у которого можно будет вызвать функцию. Если используете KodeinAware — укажите также и ему контекст (переопределите соответствующее свойство и передайте контекст Android в функцию kcontext()).

    class MyController(androidContext: Context) : KodeinAware {
        override val kodein by androidContext.closestKodein()
        override val kodeinContext = kcontext(androidContext)
        val inflater: LayoutInflater by instance()
    }
    

    Создаем отдельный Kodein в Activity


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

    class MyActivity : Activity(), KodeinAware {
        private val parentKodein by closestKodein() 
        override val kodein: Kodein by Kodein.lazy { 
            extend(parentKodein) 
            /* новые зависимости */
        }
    }
    

    Kodein, который переживает смену конфигурации


    Да, можно и так. Для этого есть функция retainedKodein. При её использовании объект Kodein не будет повторно создаваться после изменения конфигурации.

    class MyActivity : Activity(), KodeinAware {
        private val parentKodein by closestKodein()
        override val kodein: Kodein by retainedKodein { 
            extend(parentKodein)
        }
    }
    

    О чем не сказано в статье?


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

    • Scopes
    • Instance binding
    • Multi-binding
    • OnReady callbacks
    • External Source
    • Erased version pitfalls
    • Configurable Kodein
    • JSR-330 Compability

    Ну и ссылки на документацию:


    Дочитавшим спасибо, надеюсь, статья окажется вам полезной!
    Поделиться публикацией

    Похожие публикации

    Комментарии 0

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

    Самое читаемое