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

Властелин модулей. Белгород

Властелин модулей. Белгород

Alexander Blinov

April 13, 2019
Tweet

More Decks by Alexander Blinov

Other Decks in Programming

Transcript

  1. Фото с bel.ru M ODULES L ORD OF THE THE

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

    “AS IS” к “TO BE” 4 Динамические фиче-модули 5 Подведение итогов Содержание bit.ly/2DdVZTc Слайды 3
  3. Изоляция фичей Touched by His Noodly Appendage, a parody of

    Michelangelo's The Creation of Adam, is an iconic image of the Flying Spaghetti Monster[1] by Arne Niklas Jansson. 7
  4. Связи в приложении Module Module Module Module Module Module Module

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

    2 Легко отделить и тиражировать фичу для экспериментов 3 Наличие механизм навигации (диплинки / апплинки) 4 Низкая связность фичей Ожидания от подхода Стабильная работа системы 5 13
  6. Bussines vs Core Business Core •Логическая законченность •Максимальная независимость •Четкие

    внешние зависимости Решает конкретную бизнес задачу Практически не относится к бизнес логике приложения • Авторизация • Профиль • Резюме • Список вакансий • Base UI • List & Pagination • Metrics & Analytics • Network 14
  7. Слои модулей 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 15
  8. External Internal API External Internal API External Internal API Взаимодействие

    через Mediator External Deps Internal implementation Feature API APP Mediator Deps 17
  9. External Internal API APP Mediator External Internal API Deps interface

    PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) } 21
  10. 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 // … } 22
  11. 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) } 23
  12. 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) } 24
  13. 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) } 25
  14. PositionMediator PositionComponent API External Internal Component Holder Stub API PositionComponent

    API External Internal External Internal API APP Mediator External Internal API Deps MainSearchScreenMediator Component Holder MainSearchScreenComponent API External Stub API Internal 26
  15. 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.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } 27
  16. 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.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } 28
  17. 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.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } 29
  18. 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.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } 30
  19. 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.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } 31
  20. 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.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } 32
  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.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } PositionMediator External Internal API APP Mediator External Internal API Deps Component Holder PositionComponent API External Stub API Internal apiStub SingleComponentHolder PositionComponent PositionDependencies 33
  22. External Internal API APP Mediator External Internal API Deps Устройство

    DI External Deps Internal implementation Feature API Common Deps 36
  23. Feature Position Some Else Feature AppRootScope RootPositionScope AppScope RootSomeElseScope SomeElsePositionScope

    AbcScope GhiScope JklScope DefScope Глубина скоупов зависимостей External Deps Common Deps 37
  24. class AutosearchFragment : AutosearchView { @InjectPresenter lateinit var presenter: AutosearchPresenter

    @ProvidePresenter fun providePresenter(): AutosearchPresenter { return AutosearchDI.openAutosearchScope().getInstance(AutosearchPresenter::class.java) } DI внутри модуля 38
  25. Скоупы зависимостей в модулях AutosearchDI Other App part Instal Dependencies

    Open Scope Инициализация & хранение 1 0 AutosearchFragment 39
  26. Скоупы зависимостей в модулях AutosearchDI Other App part AutosearchFragment Kill

    static Open Scope Переоткрытие System Force Initializer 1 Need Autosearch Deps 2 Init Autosearch Deps 3 0 4 Instal Dependencies 40
  27. Внутри модуля Между модулями Старина Cicerone Smart Router A B

    State Machine C Навигация Через Dependencies Средства Android A B goo.gl/Tnqw2a goo.gl/tDmzEF 42
  28. External Internal API APP Mediator External Internal API Deps interface

    PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) fun onClosePositionScreen() } interface PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) fun onClosePositionScreen() } Закрытие фичи 43
  29. interface OnBackPressedListener { /** * @return true, если команда была

    обработана */ fun onBackPressed(): Boolean } interface OnBackPressable { fun addOnBackPressedListener(listener: OnBackPressedListener) fun removeOnBackPressedListener(listener: OnBackPressedListener) } BaseFragment : Fragment(), OnBackPressable, OnBackPressedListener{} Кнопка Back Обработка нажатия 44
  30. 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; //… } Core Applicant Entity Employer Entity Common Entity + Большие объекты 45
  31. Предметная область 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 Итоговая картина 46
  32. 1 Команда 3 человека, которая увеличится до 10 2 2

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

    Applicant code Employer code Common code Кодовая база Applicant Employer Common Иерархия Flavors Modules Разделение проектов ШАГ №3 52
  34. 1 Выносить Core фичи легко и весело 2 Core представляет

    иерархию модулей Вынос Core модулей ШАГ №5 54
  35. 1 Замена транзакции фрагментов на Cicerone 2 Замена Activity на

    Activity + Fragment Делаем механизм для роутинга (Smart Router) 3 Не занимайтесь внедрением MVP / MVVM / MVI на этом шаге Заменяем Activity + Fragment на Fragment 4 Слезаем с Multi Activity ШАГ №6 56
  36. Feature 1 Feature 2 Feature 3 Feature 4 App 2

    Core Feature 2 Core Feature 3 Core Features Business Features App Core Feature 1 App 1 Отпочкование фичей ШАГ №7 App 1 Feature 5 Feature 6 Feature 7 Feature 8 Feature 9 Feature ∞ 57
  37. 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 58
  38. 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 59
  39. 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 60
  40. 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 } // ... } PositionMediator Component Holder PositionComponent External Stub API API Internal 61
  41. 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 } // ... } PositionMediator Component Holder PositionComponent External Stub API API Internal 62
  42. 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 } // ... } PositionMediator Component Holder PositionComponent External Stub API API Internal 63
  43. 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 } // ... } Initializer 64
  44. Feature 6 Feature 5 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 Формирование медиаторов ШАГ №8 Иерархичная связь 68
  45. Формирование медиаторов Отпочкование фичей Слезаем с Multi Activity Вынос Core

    модулей Подготовка DI Разделение проектов Реанимация UI тестов Анализ слабых мест №8 №7 №1 №2 №3 №4 №5 №6 Photo by Sven Mieke on Unsplash План перехода на модульную архитектуру 69
  46. Мысли и инсайды Создание тестовых App модулей для фичи Описание

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

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

    App 2 Core Feature 1 Core Feature 2 Dynamic Delivery Feature 1 Новая иерархия модулей 75
  49. Dynamic Delivery Feature Runner External API ? - Activity -

    Broadcast Receiver - Service - Content Provider APP Deps Dynamic Delivery Feature Internal Provider Передача зависимостей Динамические фичи 77
  50. Dynamic Delivery Feature APP Deps Dynamic Delivery Feature Runner External

    API Internal Provider Runner Manager Передача зависимостей Динамические фичи 78
  51. 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 Загрузка модулей 79
  52. Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner

    Internal Provider Manager 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 } } } Загрузка модулей 80
  53. 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 Загрузка модулей 81
  54. 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 Загрузка модулей 82
  55. Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner

    Internal Provider Manager 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()) } } Загрузка модулей 83
  56. Dynamic Delivery Feature Dynamic Delivery Feature Runner External API Runner

    Internal Provider Manager 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()) } } Загрузка модулей 84
  57. 1 Android App Bundles 2 Конфликтует с sideloading 3 Конфликтует

    с инструментами меняющими таблицу ресурсов 4 Для работы необходимы Android 5.0 (API level 21) и последняя версия приложения Play Store 5 Прочие баги, ограничения и недоработки https://developer.android.com/guide/app-bundle/#known_issues Ограничения С сайта Android Developers 85
  58. 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 Динамические фиче-модули Выводы 87
  59. Подход решает поставленные задачи Концепция сложная и подойдет для зрелых

    проектов Выводы Экспериментируйте. В экспериментах рождается истина 89
  60. Выводы Подход решает поставленные задачи Концепция сложная и подойдет для

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