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

Властелин модулей (Mobius)

Alexander Blinov
December 08, 2018
1.7k

Властелин модулей (Mobius)

Alexander Blinov

December 08, 2018
Tweet

Transcript

  1. 1 Обзор проблем 2 Картина “TO BE” 3 Переход от

    “AS IS” к “TO BE” 4 Динамические фиче-модули 5 Подведение итогов Слайды Содержание 3 goo.gl/sQMfda
  2. Связи в приложении 6 Module Module Module Module Module Module

    Module Module Module Module Module Module Module Module Зависимости сборки Иерархия view Время жизни скоупов
  3. Разбиение на фиче-модули 1 В фиче доступны все необходимые зависимости

    2 Код фичи легко отделить и тиражировать для экспериментов 3 Наличие механизм навигации (диплинки / апплинки) 4 10 Низкая связность фичей Ожидания от подхода
  4. Что такое Feature? 11 Business Feature Core Feature Характеристика Программного

    модуля • Логически законченный • Максимально независимый • Имеет четко обозначенные внешние зависимости • Решает конкретную бизнес задачу • Логически законченный • Максимально независимый • Имеет четко обозначенные внешние зависимости • Практически не относится к бизнес логике приложения Пример фичи • Авторизация • Профиль • Резюме • Base UI • List & Pagination • Metrics & Analytics • Network Логические слои Включает все слои (по clean architecture) логики Произвольные части Программный модуль со следующими характеристиками:
  5. Слои модулей Feature 1 Feature 2 Feature 3 Feature 4

    App 1 App 2 Core Feature 2 Core Feature 3 Core Features Business Features App 12 Core Feature 1
  6. External Internal API External Internal API External Internal API Взаимодействие

    через Mediator External Deps Internal implementation Feature API APP Mediator Deps 14 Business Feature
  7. Инициализация фичи 18 External Internal API APP Mediator External Internal

    API Deps interface PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) }
  8. Инициализация фичи 19 External Internal API APP Mediator External Internal

    API Deps goo.gl/BQHXEM goo.gl/9c61dz goo.gl/rej6kN 1 Dagger 2 2 Toothpick 3 . . .
  9. Feature Position Some Else Feature Глубина скоупов зависимостей 20 AppRootScope

    PositionDepsScope AppScope SomeElseScope SomeElsePositionScope AbcScope GhiScope JklScope DefScope
  10. Инициализация фичи 21 External Internal API APP Mediator External Internal

    API Deps interface FeatureApi { fun getPositionFragment(): Fragment } interface FeatureApi { fun getPositionFragment(): Fragment //Допустимые внешние зависимости fun getSomeIntent(): SomeIntent fun getSomeInteractor(): SomeInteractor fun getSomeRepository(): SomeRepository // … }
  11. Инициализация фичи 22 External Internal API APP Mediator External Internal

    API Deps class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” } init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, ROOT_SCOPE) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(ROOT_SCOPE) } val api: FeatureApi = FeatureApiIml(ROOT_SCOPE) }
  12. Инициализация фичи 23 External Internal API APP Mediator External Internal

    API Deps class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” } init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, ROOT_SCOPE) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(ROOT_SCOPE) } val api: FeatureApi = FeatureApiIml(ROOT_SCOPE) }
  13. Инициализация фичи 24 External Internal API APP Mediator External Internal

    API Deps class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” } init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, ROOT_SCOPE) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(scopeName) } val api: FeatureApi = FeatureApiIml(ROOT_SCOPE) }
  14. Инициализация фичи 25 External Internal API APP Mediator External Internal

    API Deps MainSearchScreenMediator Component Holder MainSearchScreenComponent API External PositionMediator Component Holder Stub API Stub API Internal PositionComponent API External Internal PositionComponent API External Internal
  15. Инициализация фичи 26 External Internal API APP Mediator External Internal

    API Deps class PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String() { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPositionFragment() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPositionFragment(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  16. Инициализация фичи 27 External Internal API APP Mediator External Internal

    API Deps class PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String() { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPositionFragment() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPositionFragment(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  17. Инициализация фичи 28 External Internal API APP Mediator External Internal

    API Deps class PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String() { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPositionFragment() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPositionFragment(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  18. Инициализация фичи 29 External Internal API APP Mediator External Internal

    API Deps class PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String() { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPositionFragment() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPositionFragment(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  19. Инициализация фичи 30 External Internal API APP Mediator External Internal

    API Deps class PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String() { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPositionFragment() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPositionFragment(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  20. Инициализация фичи 31 External Internal API APP Mediator External Internal

    API Deps class PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String() { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPositionFragment() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPositionFragment(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  21. class PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder:

    SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String() { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPositionFragment() } override fun setCurrentPosition(p: String){ mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPositionFragment(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } Инициализация фичи PositionMediator Component Holder PositionComponent API External Stub API Internal 32 External Internal API APP Mediator External Internal API Deps apiStub SingleComponentHolder PositionComponent PositionDependencies
  22. Навигация 1 Навигация в фичи и App модулях 2 Навигация

    за предел фичи через Deps Старина Cicerone Smart Router A B 33 Основные принципы goo.gl/Tnqw2a State Machine C goo.gl/tDmzEF
  23. interface PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) fun

    onClosePositionScreen() } interface PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) fun onClosePositionScreen() } 34 Навигация External Internal API APP Mediator External Internal API Deps Закрытие фичи
  24. Навигация 35 Туда и обратно interface OnBackPressedListener { /** *

    @return true, если команда была обработана */ fun onBackPressed(): Boolean } interface OnBackPressable { fun addOnBackPressedListener(listener: OnBackPressedListener) fun removeOnBackPressedListener(listener: OnBackPressedListener) } BaseFragment : Fragment(), OnBackPressable, OnBackPressedListener{}
  25. Навигация public class Resume { private Collection<SpecializationDto> specializations; private ProfessionalExperienceDto

    professionalExperience; private Set<ResumeCertificateDto> certificates; private AdditionalEducationDto additionalEducation; private BusinessTripReadiness businessTripsReadiness; private ContactInformationDto contactInformation; private RecommendationInfoDto recommendationInfo; private List<UserImageDto> userPortfolio; private EducationHistoryDto educationHistory; private RelocationInfoDto relocationInfo; private ResumeLanguageDto languages; private WorkExperience workExperience; private Set<Employment> employments; private EducationLevel education; private LocalDateTime creationTime; private LocalDateTime lastChangeTime; private Set<Schedule> schedules; private RoadTimeType roadTime; private ResumeStatus resumeStatus; private UserImageDto userImage; private AreaDto area; private AccessDto access; private LocalDate birthdayDate; private AreaDto[] citizenship; private AreaDto[] workTickets; private MoneyDto desirableCompensation; private MetroDto nearestMetro; private Gender gender; private String[] keySkills; private String hash; private String title; private String aboutMe; private int id; private int siteId; private boolean hasVehicle; private String[] driverLicense; //… } Большие объекты 36 Core Applicant Entity Employer Entity Common Entity +
  26. Итоговая картина Предметная область Applicant Entity Employer Entity Common Entity

    Общий функционал Фиче-модули User Job Position Push Suggestion University … Base UI List Processing Pagination Network Remote Config Custom Views View#1 View#2 View#3 … … Приложения Applicant Application Russia Azerbaijan Belarus Employer Application Russia Belarus 37
  27. Контекст 1 Команда 3 человека, которая увеличится до 10 2

    2 приложения на одной кодовой базе в (!) одном модуле 3 Multi-Activity подход и монолитные Activity и Fragment 4 Dagger 2 и пухлый ApplicationScope 5 Отсутствие Event Bus, наличие синглтонов 6 End2end тесты основных сценариев на голом Espresso 39
  28. Шаг 1. Отделение Applicant и Employer 43 Application Applicant code

    Employer code Common code Кодовая база Иерархия Applicant code Employer code Common code Кодовая база Applicant Employer Common Иерархия Flavors Modules
  29. Шаг 2. Подготовка DI Dagger и гибкость 44 DI фреймворки

    можно комбинировать Временная(?) Замена Dagger2 на Toothpick
  30. Шаг 3. Вынос Core модулей 45 1 Выносить Core фичи

    легко и весело 2 Core представляет иерархию модулей
  31. 47 Шаг 4. Слезаем с Multi Activity Действия 1 Замена

    транзакции фрагментов на Cicerone 2 Замена Activity на Activity + Fragment Делаем механизм для роутинга (Smart Router) 3 Не занимайтесь внедрением MVP / MVVM / MVI на этом шаге Заменяем Activity + Fragment на Fragment 4
  32. Шаг 5. Отпочкование фичей Feature 1 Feature 2 Feature 3

    Feature 4 App 2 Core Feature 2 Core Feature 3 Core Features Business Features App 48 Core Feature 1 App 1 App 1 Feature 5 Feature 6 Feature 7 Feature 8 Feature 9 Feature ∞ Толстяк App модуль
  33. 49 class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const

    val ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” private val counter = AtomicInteger() } private val scopeName = ROOT_SCOPE + counter.incrementAndGet() init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, scopeName) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(scopeName) } val api: FeatureApi = FeatureApiIml(scopeName) } Переинициализация зависимостей Шаг 5. Отпочкование фичей Переиспользование PositionMediator Component Holder PositionComponent External Stub API API Internal
  34. class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val

    ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” private val counter = AtomicInteger() } private val scopeName = ROOT_SCOPE + counter.incrementAndGet() init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, scopeName) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(scopeName) } val api: FeatureApi = FeatureApiIml(scopeName) } 50 Шаг 5. Отпочкование фичей Переиспользование PositionMediator Component Holder PositionComponent External Stub API API Internal
  35. class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val

    ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” private val counter = AtomicInteger() } private val scopeName = ROOT_SCOPE + counter.incrementAndGet() init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, scopeName) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(scopeName) } val api: FeatureApi = FeatureApiIml(scopeName) } PositionMediator Component Holder PositionComponent External Stub API API Internal 51 Шаг 5. Отпочкование фичей Переиспользование
  36. 52 class PositionScreenMediator() { private val componentHolder: MultiComponentHolder<PositionComponent, PositionDependencies> =

    MultiComponentHolder { deps -> PositionComponent(deps) } fun initComponent(key: String, deps: PositionDependencies) { componentHolder.initComponent(key, deps) } fun provideApiStub(key: String): FeatureApi { return componentHolder.provideComponent(key).api } // ... } Шаг 5. Отпочкование фичей Переиспользование PositionMediator Component Holder PositionComponent External Stub API API Internal
  37. 53 Шаг 5. Отпочкование фичей Переиспользование PositionMediator Component Holder PositionComponent

    External Stub API API Internal class PositionScreenMediator() { private val componentHolder: MultiComponentHolder<PositionComponent, PositionDependencies> = MultiComponentHolder { deps -> PositionComponent(deps) } fun initComponent(key: String, deps: PositionDependencies) { componentHolder.initComponent(key, deps) } fun provideApiStub(key: String): FeatureApi { return componentHolder.provideComponent(key).api } // ... }
  38. 54 class PositionScreenMediator() { private val componentHolder: MultiComponentHolder<PositionComponent, PositionDependencies> =

    MultiComponentHolder { deps -> PositionComponent(deps) } fun initComponent(key: String, deps: PositionDependencies) { componentHolder.initComponent(key, deps) } fun provideApiStub(key: String): FeatureApi { return componentHolder.provideComponent(key).api } // ... } Шаг 5. Отпочкование фичей Переиспользование PositionMediator Component Holder PositionComponent External Stub API API Internal
  39. class PositionScreenMediator() { private val componentHolder: MultiComponentHolder<PositionComponent, PositionDependencies> = MultiComponentHolder

    { deps -> PositionComponent(deps) } fun initComponent(key: String, deps: PositionDependencies) { componentHolder.initComponent(key, deps) } fun provideApiStub(key: String): FeatureApi { return componentHolder.provideComponent(key).api } // ... } 55 Шаг 5. Отпочкование фичей Переиспользование PositionMediator Component Holder PositionComponent External Stub API API Internal Initializer
  40. Feature 5 Feature 6 Шаг 6. Формирование медиаторов 58 Возможная

    (!) иерархия фичей Feature 1 Feature 2 Feature 3 Feature 4 App 1 App 2 Core Feature 2 Core Feature 3 Core Features Business Features App Core Feature 1 Mediator
  41. Мысли и инсайды 60 Создание тестовых App модулей для фичи

    Описание архитектуры в вики / статье для понимания Проведение экспериментов на Console проекте Время сборки и борьба с IDE
  42. Причины использования 62 Основной функционал 5 mb 40mb VR модуль

    100% 10% Частота использования Размер https://developer.android.com/guide/app-bundle/ https://developer.android.com/studio/projects/dynamic-delivery
  43. Иерархия 63 Feature 1 Feature 2 Feature 3 Feature 4

    App 1 App 2 Core Feature 1 Core Feature 2 Dynamic Delivery Feature 1 Всё пропало, шеф
  44. ? - Activity - Broadcast Receiver - Service - Content

    Provider 65 Dynamic Delivery Feature APP Deps Dynamic Delivery Feature Runner External API Internal Взаимодействие dynamic feature
  45. 66 Dynamic Delivery Feature APP Deps Dynamic Delivery Feature Runner

    External API Internal Provider Взаимодействие dynamic feature Runner Manager
  46. Загрузка модулей 67 class SplitFeatureRunner(private val context: Context) { private

    val splitInstallManager: SplitInstallManager = SplitInstallManagerFactory.create(context).also { it.registerListener { status -> doSomething(status) } } fun installModule() { val request = SplitInstallRequest .newBuilder() .addModule(“dynamic_feature_vr”) .build(); splitInstallManager .startInstall(request) .addOnSuccessListener { sessionId -> Intent().setClassName(packageName, serviceClassName) .also { startService(it) } } .addOnFailureListener { exception -> // something on fail } } } Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner Internal Provider Manager
  47. Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner

    Internal Provider Manager Загрузка модулей 68 class SplitFeatureRunner(private val context: Context) { private val splitInstallManager: SplitInstallManager = SplitInstallManagerFactory.create(context).also { it.registerListener { status -> doSomething(status) } } fun installModule() { val request = SplitInstallRequest .newBuilder() .addModule(“dynamic_feature_vr”) .build(); splitInstallManager .startInstall(request) .addOnSuccessListener { sessionId -> Intent().setClassName(packageName, serviceClassName) .also { startService(it) } } .addOnFailureListener { exception -> // something on fail } } }
  48. Загрузка модулей 69 class SplitFeatureRunner(private val context: Context) { private

    val splitInstallManager: SplitInstallManager = SplitInstallManagerFactory.create(context).also { it.registerListener { status -> doSomething(status) } } fun installModule() { val request = SplitInstallRequest .newBuilder() .addModule(“dynamic_feature_vr”) .build(); splitInstallManager .startInstall(request) .addOnSuccessListener { sessionId -> Intent().setClassName(packageName, serviceClassName) .also { sartService(it) } } .addOnFailureListener { exception -> // something on fail } } } Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner Internal Provider Manager
  49. Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner

    Internal Provider Manager Загрузка модулей 70 class SplitFeatureRunner(private val context: Context) { private val splitInstallManager: SplitInstallManager = SplitInstallManagerFactory.create(context).also { it.registerListener { status -> doSomething(status) } } fun installModule() { val request = SplitInstallRequest .newBuilder() .addModule(“dynamic_feature_vr”) .build(); splitInstallManager .startInstall(request) .addOnSuccessListener { sessionId -> Intent().setClassName(packageName, serviceClassName) .also { startService(it) } } .addOnFailureListener { exception -> // something on fail } } }
  50. Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner

    Internal Provider Manager 71 Взаимодействие dynamic feature object DynamicFeatureModuleManager { private var listenerSet: MutableSet<CheckInstallListener> = mutableSetOf() fun setLauncher(launcher: DynamicFeatureLauncher) { for (listener in listenerSet) { listener.onCheckInstalled(launcher) } } } fun addListener(checkInstallListener: CheckInstallListener) { listenerSet.add(checkInstallListener) } // . . . } open class LaunchModuleService : IntentService("LaunchModuleService") { override fun onHandleIntent(intent: Intent?) { DynamicFeatureModuleManager.setLauncher(DynamicFeatureLauncherImpl()) } }
  51. Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner

    Internal Provider Manager 72 Взаимодействие dynamic feature object DynamicFeatureModuleManager { private var listenerSet: MutableSet<CheckInstallListener> = mutableSetOf() fun setLauncher(launcher: DynamicFeatureLauncher) { for (listener in listenerSet) { listener.onCheckInstalled(launcher) } } } fun addListener(checkInstallListener: CheckInstallListener) { listenerSet.add(checkInstallListener) } // . . . } open class LaunchModuleService : IntentService("LaunchModuleService") { override fun onHandleIntent(intent: Intent?) { DynamicFeatureModuleManager.setLauncher(DynamicFeatureLauncherImpl()) } }
  52. Ограничения 73 1 Android App Bundles 2 Не поддерживает “APK

    expansion files” и APK > 100 mb 3 Конфликтует с инструментами меняющими таблицу ресурсов 4 Для работы необходимы Android 5.0 (API level 21) и последняя версия приложения Play Store 5 Прочие баги, ограничения и недоработки На сайте Android Developers https://developer.android.com/guide/app-bundle/#known_issues
  53. Динамические фиче-модули 75 Feature 1 Feature 2 Feature 3 Feature

    4 App 1 App 2 Core Feature 1 Core Feature 2 Dynamic Delivery Feature 1 Dynamic Delivery Feature Runner 1 50± kb 40mb Выводы
  54. Самое сложное - выстроить связи модулей. Выстроили = Победили Концепция

    сложная и подойдет для зрелых проектов Экспериментируйте. В экспериментах рождается истина Выводы 77
  55. Выводы Самое сложное - выстроить связи модулей. Выстроили = Победили

    Концепция сложная и подойдет для зрелых проектов Экспериментируйте. В экспериментах рождается истина 78 Обзор проблем Картина “TO BE” Подведение итогов Слайды Контакты: Переход от “AS IS” к “TO BE” Динамические фиче-модули О чем мы говорили t.me/xanderblinov