Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Быстрая разработка однотипного UI с помощью под...

Быстрая разработка однотипного UI с помощью подмены зависимостей у экрана

Перевалов Данил, ЦИАН at MOSDROID #18 Argon

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

MOSDROID

July 19, 2019
Tweet

More Decks by MOSDROID

Other Decks in Programming

Transcript

  1. Выделяем ListMapper Mapper который превратит модели полученные из GetListUseCase в

    то, что может обработать наш общий presenter. interface ListMapper<T> { fun mapToUiModel(items: List<T>): List<ListUiModel> } 16
  2. Модель для отображения элемента 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
  3. Формируем зависимости экрана interface SearchListDependencies { fun provideGetListUseCase(): GetListUseCase<*> fun

    provideSelectItemProcessor(): SelectItemProcessor fun provideListMapper(): ListMapper<*> } 19
  4. Проблема. 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
  5. Проблема. 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
  6. Проблема. AndroidX FragmentFactory class MyFragmentFactory () : FragmentFactory() { override

    fun instantiate(loader: ClassLoader, className: String): Fragment { val fragmentClass = loadFragmentClass(loader, className) return fragmentClass.newInstance() } } 25
  7. 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
  8. 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
  9. Открытие экрана 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
  10. Добавим 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
  11. Добавим 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
  12. Setter. Итог Плюсы • Хорошо дружит с модулями • Очень

    гибко. Можно наделать хоть 100 штук в зависимости от нужд Минусы • Часть данных подается через Bundle, другая часть через setter. • Приходится делать хранилище зависимостей. • Приходится делать систему по подаче зависимостей на экран. • Надо как-то понимать, что пора удалить зависимость из хранилища. 36
  13. Как это будет работать? 38 interface GetListUseCase<T> { fun getList(uid

    : String, query: String) : List<T> } class SearchListPresenter(...) { private val uidFromBundle: String fun loadList() { val list = getListUseCase.getList(uidFromBundle, query) } }
  14. Внутри реализации GetListUseCase 39 class GetListUseCaseFactory<*> { private val storage

    = HashMap<String, GetListUseCase> 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) } } }
  15. Аутсорс. Итог 41 Плюсы • Делать очень просто и быстро.

    Если у вас всего один такой кейс, то лучше выбрать этот способ. Минусы • Методы приходится “марать”, добавляя им передачу uid, либо добавляя дополнительный метод для его передачи. • Есть класс который знает о всех возможных вариантах зависимостей. Можно исправить, сделав фабрику абстрактной, но это уже как-то слишком сложно для такой простой вещи. • Если у вас уже есть система подачи зависимостей (Dagger, Koin), то немного странно выглядит класс, который, по сути, становится прослойкой. • Надо как-то понимать, что пора удалить зависимость из хранилища.
  16. Как работает 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
  17. Передаем через parcelable. Reflection private const val SHARED_COMPONENT = "shared.component"

    fun <T> Bundle.putSharedComponent(sharedComponent: SharedComponentCreator<T>) { putString(SHARED_COMPONENT, sharedComponent::class.java.name) } fun <T> Bundle.readSharedComponent(): SharedComponentCreator<T> { val kotlinClass = Class.forName(getString(SHARED_COMPONENT)).kotlin val instance = kotlinClass.createInstance() return instance as SharedComponentCreator<T> } 46
  18. Реализация руками class HighwayListCreator:SharedComponentCreator<SearchListDependencies>() { override fun build(): SearchListDependencies {

    return object : SearchListDependencies { override fun provideGetListUseCase() = HighwayListUseCase() override fun provideSelectItemProcessor() = HighwaySelectProcessor() override fun provideListMapper() = HaghwayListMapper() } } } 47
  19. Открытие экрана 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
  20. Получаем зависимости на экране 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
  21. Bundle. Итог 52 Плюсы • Дружит с Dagger. Можно создать

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

    схоже Минусы: • Нужны зачатки UI кита, ну или очень понимающие дизайнеры 53