Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Решаемая проблема 2 https://api.cian.ru/get-village VillageResponse Нельзя поставить Like https://api.cian.ru/get-complex ComplexResponse Можно поставить Like

Slide 3

Slide 3 text

Решаемая проблема 3

Slide 4

Slide 4 text

Может копипаста? 4

Slide 5

Slide 5 text

Не стоит применять копипасту!!! 5

Slide 6

Slide 6 text

Типичные способы решения Далеко не самые лучшие 6

Slide 7

Slide 7 text

Способ 1. GodPresenter и GodFragment 7

Slide 8

Slide 8 text

Проблема 1. Модуль очень много знает... 8

Slide 9

Slide 9 text

Проблема 2. Рост все ломает 9

Slide 10

Slide 10 text

Способ 2. Наследование 10

Slide 11

Slide 11 text

Проблема 1. Двойное наследование 11

Slide 12

Slide 12 text

Проблема 2. О модуле много кто знает... 12

Slide 13

Slide 13 text

Как сделать лучше? Придется добавить композиции 13

Slide 14

Slide 14 text

Что нужно сделать 14 Общий код Общий код

Slide 15

Slide 15 text

Выделяем GetListUseCase UseCase для получения списка моделей. interface GetListUseCase { fun getList(query: String) : List } 15

Slide 16

Slide 16 text

Выделяем ListMapper Mapper который превратит модели полученные из GetListUseCase в то, что может обработать наш общий presenter. interface ListMapper { fun mapToUiModel(items: List): List } 16

Slide 17

Slide 17 text

Модель для отображения элемента data class ListUiModel( val id: Long, val image: Image? = null, val text: Text ) data class Image( @DrawableRes val imageRes: Int, @ColorInt val tintColor: Int? ) data class Text( val text: String, val endText: String, @ColorRes val colorRes: Int ) 17

Slide 18

Slide 18 text

Выделяем SelectItemProcessor Будет обрабатывать реакцию на клик interface SelectItemProcessor { fun selectItem(id: Long) } 18

Slide 19

Slide 19 text

Формируем зависимости экрана interface SearchListDependencies { fun provideGetListUseCase(): GetListUseCase<*> fun provideSelectItemProcessor(): SelectItemProcessor fun provideListMapper(): ListMapper<*> } 19

Slide 20

Slide 20 text

Реализации 20

Slide 21

Slide 21 text

Как хотелось бы это видеть 21

Slide 22

Slide 22 text

И вроде все хорошо... но теперь надо как-то передать зависимости на экран 22

Slide 23

Slide 23 text

Проблема. Android сам создает Activity ClassLoader classLoader = appContext.getClassLoader(); activity = mInstrumentation.newActivity( classLoader, component.getClassName(), activityClientRecord.intent ); activityClientRecord.intent.setExtrasClassLoader(classLoader); activityClientRecord.intent.prepareToEnterProcess(); if (activityClientRecord.state != null) { activityClientRecord.state.setClassLoader(classLoader); } 23

Slide 24

Slide 24 text

Проблема. Android сам создает Fragment if (container != null) { this.mInstance = container.instantiate( context, this.mClassName, this.mArguments ); } else { this.mInstance = Fragment.instantiate( context, this.mClassName, this.mArguments ); } 24

Slide 25

Slide 25 text

Проблема. AndroidX FragmentFactory class MyFragmentFactory () : FragmentFactory() { override fun instantiate(loader: ClassLoader, className: String): Fragment { val fragmentClass = loadFragmentClass(loader, className) return fragmentClass.newInstance() } } 25

Slide 26

Slide 26 text

Проблема с созданием 26

Slide 27

Slide 27 text

И как от этого избавиться? Есть пара способов 27

Slide 28

Slide 28 text

Способ 1. Setter 28

Slide 29

Slide 29 text

Setter для Activity class DependencySetter( private val dependencyStorage: DependencyStorage ): Application.ActivityLifecycleCallbacks { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { if (activity is SearchListActivity) { val dependencies = dependencyStorage.getSearchListDependencies() activity.setSearchListDependencies(dependencies) } } } 29

Slide 30

Slide 30 text

Setter для Fragment class DependencySetter( private val dependencyStorage: DependencyStorage ): FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentCreated(m: FragmentManager, fragment: Fragment) { if (fragment is SearchListFragment) { val dependencies = dependencyStorage.getSearchListDependencies() fragment.setSearchListDependencies(dependencies) } } } 30

Slide 31

Slide 31 text

Проблема. Как отличить? 31

Slide 32

Slide 32 text

Открытие экрана override fun openSelectFormHighway(activity: Activity) { val intent = Intent(activity, SearchListActivity::class.java) val extras = Bundle() val uid = UUID.randomUUID().toString() extras.putString(KEY_UID, uid) intent.putExtras(extras) dependencyStorage.putSearchListDependencies(uid, highwayDependencies) activity.startActivity(intent) } 32

Slide 33

Slide 33 text

Добавим uid. Activity private const val KEY_UID = ".key.uid" class DependencySetter( private val dependencyStorage: DependencyStorage ): Application.ActivityLifecycleCallbacks { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { if (activity is SearchListActivity) { val intent = activity.intent val uid = intent.getStringExtra(KEY_UID)!! val dependencies = dependencyStorage.getSearchListDependencies(uid) activity.setSearchListDependencies(dependencies) } } } 33

Slide 34

Slide 34 text

Добавим uid. Fragment private const val KEY_UID = ".key.uid" class DependencySetter( private val dependencyStorage: DependencyStorage ): FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentCreated(m: FragmentManager, fragment: Fragment) { if (fragment is SearchListFragment) { val bundle = fragment.arguments val uid = bundle.getString(KEY_UID)!! val dependencies = dependencyStorage.getSearchListDependencies(uid) fragment.setSearchListDependencies(dependencies) } } } 34

Slide 35

Slide 35 text

Отношения с модулями 35

Slide 36

Slide 36 text

Setter. Итог Плюсы ● Хорошо дружит с модулями ● Очень гибко. Можно наделать хоть 100 штук в зависимости от нужд Минусы ● Часть данных подается через Bundle, другая часть через setter. ● Приходится делать хранилище зависимостей. ● Приходится делать систему по подаче зависимостей на экран. ● Надо как-то понимать, что пора удалить зависимость из хранилища. 36

Slide 37

Slide 37 text

Способ 2. Аутсорс 37

Slide 38

Slide 38 text

Как это будет работать? 38 interface GetListUseCase { fun getList(uid : String, query: String) : List } class SearchListPresenter(...) { private val uidFromBundle: String fun loadList() { val list = getListUseCase.getList(uidFromBundle, query) } }

Slide 39

Slide 39 text

Внутри реализации GetListUseCase 39 class GetListUseCaseFactory<*> { private val storage = HashMap override fun getList(uid : String, query: String) : List<*> { return storage.getOrElse(uid, { createNewUseCase(uid) }).getList(query) } private fun createNewUseCase(uid : String) : GetListUseCase { return when(getTypeByUid(uid)) { HIGHWAY -> highwayListUseCase REGION -> regionListUseCase }.also { storage.put(uid, it) } } }

Slide 40

Slide 40 text

Отношения с модулями 40

Slide 41

Slide 41 text

Аутсорс. Итог 41 Плюсы ● Делать очень просто и быстро. Если у вас всего один такой кейс, то лучше выбрать этот способ. Минусы ● Методы приходится “марать”, добавляя им передачу uid, либо добавляя дополнительный метод для его передачи. ● Есть класс который знает о всех возможных вариантах зависимостей. Можно исправить, сделав фабрику абстрактной, но это уже как-то слишком сложно для такой простой вещи. ● Если у вас уже есть система подачи зависимостей (Dagger, Koin), то немного странно выглядит класс, который, по сути, становится прослойкой. ● Надо как-то понимать, что пора удалить зависимость из хранилища.

Slide 42

Slide 42 text

Способ 3. Через Bundle 42

Slide 43

Slide 43 text

Как работает Parcelable? public final void writeParcelableCreator(Parcelable parcelable) { String name = parcelable.getClass().getName(); writeString(name); } public final Parcelable.Creator> readParcelableCreator(ClassLoader loader) { String name = readString(); ………………………………. Class> parcelableClass = Class.forName( name, false, parcelableClassLoader ); ………………………………. 43

Slide 44

Slide 44 text

Интерфейс, который создает классы interface SharedComponentCreator { fun build(): D } 44

Slide 45

Slide 45 text

Общая логика 45

Slide 46

Slide 46 text

Передаем через parcelable. Reflection private const val SHARED_COMPONENT = "shared.component" fun Bundle.putSharedComponent(sharedComponent: SharedComponentCreator) { putString(SHARED_COMPONENT, sharedComponent::class.java.name) } fun Bundle.readSharedComponent(): SharedComponentCreator { val kotlinClass = Class.forName(getString(SHARED_COMPONENT)).kotlin val instance = kotlinClass.createInstance() return instance as SharedComponentCreator } 46

Slide 47

Slide 47 text

Реализация руками class HighwayListCreator:SharedComponentCreator() { override fun build(): SearchListDependencies { return object : SearchListDependencies { override fun provideGetListUseCase() = HighwayListUseCase() override fun provideSelectItemProcessor() = HighwaySelectProcessor() override fun provideListMapper() = HaghwayListMapper() } } } 47

Slide 48

Slide 48 text

Реализация Dagger class HighwayListCreator: SharedComponentCreator() { override fun build(): SearchListDependencies { return DaggerSearchListHighwayComponent.builder() …………………….. .build() } } 48

Slide 49

Slide 49 text

Открытие экрана override fun openSelectFormHighway(activity: Activity) { val intent = Intent(activity, SearchListActivity::class.java) val extras = Bundle() val sharedComponent = SearchListHighwayComponent.Creator() extras.putSharedComponent(sharedComponent) intent.putExtras(extras) activity.startActivity(intent) } 49

Slide 50

Slide 50 text

Получаем зависимости на экране private val presenter by lazy { val creator = intent.extras.readSharedComponent() val dependencies = creator.build() return@lazy SearchListPresenter( dependencies.provideGetListUseCase(), dependencies.provideSelectItemProcessor(), dependencies.provideListMapper() ) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) presenter.someAction() } 50

Slide 51

Slide 51 text

Отношения с модулями 51

Slide 52

Slide 52 text

Bundle. Итог 52 Плюсы ● Дружит с Dagger. Можно создать компонент и унаследовать его от интерфейса и все само заработает. ● Программисту нужно будет только написать реализацию интерфейса. ● Все что нужно для экрана в итоге кладется в Bundle. Минусы ● Рефлексия. ● Если в стеке есть несколько одинаковых экранов и ваш Presenter/ViewModel/Controller не переживает ЖЦ, то все же придется добавлять uid. ● Отдельный маленький класс для каждого типа зависимостей

Slide 53

Slide 53 text

Итог Плюсы: ● Можно быстро клепать экраны которые выглядят очень схоже Минусы: ● Нужны зачатки UI кита, ну или очень понимающие дизайнеры 53

Slide 54

Slide 54 text

Спасибо за внимание! 54 Контакты: Telegram @princeparadoxes