Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

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

MOSDROID

July 19, 2019
Tweet

More Decks by MOSDROID

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. Модель для отображения элемента
    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

    View Slide

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

    View Slide

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

    View Slide

  20. Реализации
    20

    View Slide

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

    View Slide

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

    View Slide

  23. Проблема. 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

    View Slide

  24. Проблема. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  28. Способ 1. Setter
    28

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

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

    View Slide

  32. Открытие экрана
    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

    View Slide

  33. Добавим 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

    View Slide

  34. Добавим 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. Внутри реализации 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) }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. Как работает 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

    View Slide

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

    View Slide

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

    View Slide

  46. Передаем через 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

    View Slide

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

    View Slide

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

    View Slide

  49. Открытие экрана
    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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide