[Разработка мобильных приложений, Разработка под Android, Kotlin] Системный гайд по созданию White Label android-приложений
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Как написать код один раз, а продать 20 мобильных приложений? Мы нашли ответ путём проб и факапов и разложили опыт по пунктам: из статьи вы узнаете, как безболезненно реализовать White Label android-проект.
Greetings and salutations! По работе я однажды получил крутую задачу по разработке White Label android-приложения. Изучил достижения коллег в этой области и нашёл только:
- входные гайды (раз, два, три, etc) о механизмах, но без промышленного дизайна;
- статьи, в которых освещены узкие аспекты задачи (раз, два, etc).
На мой взгляд, теме не хватает цельного гайда, который проведёт от потребностей к требованиям, от требований к архитектуре и вооружит best practices. Поэтому я решил сделать его сам.1 Ставим задачуИ малый, и средний бизнес нуждается в дешёвых приложениях для систем лояльности: крупные ретейлеры уже обзавелись такими продуктами и приучили пользователей к мобильным приложениям.Работает система просто (покупатели показывают приложение на кассе вместо пластиковой карты и получают скидки и бонусы), а для её реализации нужны типовые фичи: регистрация, виртуальная дисконтная карта и прочие.Бюджет ограничен... фичи типовые... да здравствует конструктор приложений! Или White Label продукт? Пока отложим термины и опишем задачу: генерировать приложения из единой кодовой базы, каждое – с дизайном под бренд клиента и только нужными ему фичами.
Задача: создавать приложения для разных клиентов из единой кодовой базы
1.1 Визуализируем решениеПосмотрим, как это должно работать на примере экрана дисконтной карты. В магазине карту можно использовать двумя способами: чтобы узнать баланс и чтобы получить тот или иной бонус.У каждого бренда своя программа лояльности, например в SEPHORA накапливаются бонусные баллы и процент скидки, а в «Пятёрочке» только баллы. В приложениях это выглядит так:
Мы хотим поддерживать как можно больше программ лояльности, при этом интерфейс оставить общим. Предусмотрим вариативность, получим возможный вид экрана карты для разных клиентов:
Как реализовать такой проект без боли? Прочитайте статью и найдёте ответ.1.2 Детализируем требованияРазложим видение по полочкам: как в ТЗ, но проще.Функциональные требования
- Реализовать общие модули фичей:
- новости – клиент узнаёт об акциях и жизни сети магазинов;
- лояльность – получает дисконтную карту, узнаёт баланс, пробивает на кассе;
- ...
- Задавать отдельно для каждого приложения:
- наборы фичей, чтобы выбирать сами модули и настраивать их параметры;
- бренд, чтобы настраивать цвета и менять ресурсы: шрифты, картинки, зашитый контент.
Нефункциональные
- у приложений должен быть общий код;
- настройка нового приложения – меньше четырёх часов разработчика;
- архитектура должна упрощать расширение модулей и поддержку от 10 до 100 приложений.
1.3 Что пилим то? Конструктор? White Label?Требования ясны – разберёмся с терминами. На старте проекта я думал, что мы делаем «конструктор приложений». Искал статьи с опытом создания, а находил рекламу готовых продуктов. Потратил пару часов на гугл и с радостью сэкономлю время вам:
- Что даёт конструктор/платформа:
- универсальный сервис сборки приложений из готовых компонентов;
- например, AppGyver – drag’n’drop вёрстка, программирование на низкоуровневых фичах (открыть экран, сделать фото);
- творим что угодно – от приложений по покупке золота до приёмки грузов.
- Что даёт White Label:
- конструктор для конкретного типа приложений, например для такси;
- ребрендинг под клиента и настройка высокоуровневых фич (новости, профиль)
Наш фокус на системах лояльности. Значит, делаем White Label. Гуглим «white label android development» и находим то, что нужно.2 Проектируем и воплощаемСтроим системную схему White Label приложенияДля успешной реализации проекта нам нужно создать «понятную» и «расширяемую» архитектуру (раскрывать термины не буду, здесь хватит интуитивного понимания). Для этого заглянем вглубь системы, выделим подзадачи по слоям Clean Architecture…
… и получим четыре жирные проблемы:
- Как шарить кодовую базу между приложениями?
- Как сделать ребрендинг?
- Как задавать конфиги?
- Как отключать ненужные модули и настраивать необходимые?
В очередь, проблемы, в очередь!2.1 Шарим код
Задача – одна кодовая база, до 100 приложений. Решение – Gradle Product Flavors.
Если вы ещё не знакомы с Gradle Product Flavors, советую почитать документацию или общие статьи. А можно и сразу в контексте White Label: кратко или в формате инструкцииВ двух словах, создаём «варианты» базового приложения. Каждый получает изолированные папки с кодом и ресурсами, сохраняя доступ к общим из main.Главное преимущество. Относительная простота переиспользования кода и ресурсов, удобство сборки.Главный недостаток. Если вариантов больше 100, то в проекте и конфигах будет тяжело ориентироваться. Но у нас меньше, поэтому ок.Альтернативы, на мой взгляд, рассматривать нет смысла: решение надёжное, из коробки.Пример flavors. Допустим, на старте делаем два приложения:
- «Лояка» — абстрактная компания;
- «Ювелирия» — сеть ювелирных магазинов.
Назовём flavors соответственно — loyaka и jewelry. Сразу реализуем best practice — конфиг каждого flavor вынесем в отдельный файлик. Зачем? Станет ясно чуть позже.Пока создадим:
- папку project_flavors;
- в ней — gradle-скрипты flavor_loyaka.gradle, flavor_jewelry.gradle и flavors_common.gradle;
- задействуем скрипты в build.gradle уровня app.
Здесь и далее привожу сокращённые примеры из тестового проекта к статье.flavor_loyaka.gradle
apply from: "$rootDir/project_flavors/flavors_common.gradle"
android {
productFlavors {
loyaka {
dimension APP_DIMENSION
resValue "string", APP_NAME_VAR, 'Лояка'
applicationId BASE_PACKAGE + 'loyaka'
}
}
}
flavor_jewelry.gradle
apply from: "$rootDir/project_flavors/flavors_common.gradle"
android {
productFlavors {
jewerly {
dimension APP_DIMENSION
resValue "string", APP_NAME_VAR, 'Ювелирия'
applicationId BASE_PACKAGE + 'jewelry'
}
}
}
flavors_common.gradle
android {
ext.DIMENSION_APP = "app"
ext.APP_NAME_VAR = "app_name"
ext.BASE_PACKAGE = "com.livetyping."
}
Наконец, задействуем flavors — в build.gradle уровня app:
...
apply from: "$rootDir/project_flavors/flavor_loyaka.gradle"
apply from: "$rootDir/project_flavors/flavor_jewelry.gradle"
apply from: "$rootDir/project_flavors/flavors_common.gradle"
android {
...
flavorDimensions APP_DIMENSION
}
...
2.2 Перекрашиваем2.2.1 КонцептУ каждого приложения свой бренд, который складывается из:
- цветовой схемы;
- шрифтов, картинок, строк;
- зашитого контента (соглашений, ссылок в соц. сети).
Благодаря flavors тоже решим задачу просто. Загрузим в голову 3 факта:
- общие код и ресурсы проекта лежат в папке main;
- для gradle main это как дефолтный flavor;
- у каждого flavor свои исходники. Например, общие ресурсы лежат в main/res, а специфичные для флэйвора loyaka в loyaka/res;
Что произойдёт, если в main/res и loyaka/res будут картинки с одинаковым именем animal.webp? Возникнет конфликт, и чтобы решить его, Gradle переопределит базовые ресурсы кастомными. Если непонятно, поможет диаграмма:
Слева — ресурсы по flavor; справа — итоговый APK.Задача решена! Уберём дефолтные ресурсы в main, а в конкретных flavor будем переопределять по необходимости.2.2.2 Best practicesКрайне важно заранее договориться с дизайнерами:
- ресурсы в приложениях называем одинаково — вставляем в проект прямиком из дизайна;
- тему задаём чётким набором цветов — для перекрашивания копируем colors.xml в новый flavor и просто меняем значения.
И, конечно, соблюдаем договорённости, ведь впереди ждут испытания. Например, мы сразу решили, что задаём строгий набор цветов. Однако в очередном приложении цвета не сошлись — часть элементов цвета primary в новом дизайне стали accent. Сразу обсудили и изменили дизайн, а ведь могли бы и вставить костыль.Проблема в том, что костыли масштабируются на 100 приложений! Представьте, что дизайн оставили, а для решения проблемы ввели новый цвет в общей теме. Приходит новое приложение, а там опять несоответствие: вводим очередной цвет, итог — тема на сотни значений и проект, который тяжело поддерживать.
Создать и поддерживать чёткую схему для всех приложений на порядок дешевле, чем расширять код, состоящий из «уникальных» косяков.
2.2.3 Пример схемы цветовЗадаём цвета бренда в файле project_styleguide.xml:
- для «Лояки» — loyaka/res/values/project_styleguide.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="active">#68b881</color>
<color name="background">#36363f</color>
<color name="disabled">#daede0</color>
<color name="field_dark">#f5f5f5</color>
...
</resources
- для «Ювелирии» — jewelry/res/values/project_styleguide.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="active">#a160d5</color>
<color name="background">#f6ebff</color>
<color name="disabled">#e2c8f6</color>
<color name="field_dark">#f5f5f5</color>
...
</resources>
2.3 Задаём конфиг2.3.1 КонцептФичи настраиваем на двух уровнях:
- отключаем ненужные модули;
- меняем параметры внутри самих модулей.
Начнём с того, что зафиксируем правила в типовой форме и пошарим на команду. Настройка нового приложения упрощена: аналитик собирает с клиента требования и присылает форму. Разработчику остаётся механически перевести её в конфиг.Упрощённый пример дока в формат «модуль-фича-параметры»:
- Подключаемые модули:
- лояльность;
- новости;
- …
- Аутентификация:
- логин пользователя: телефон или email;
- маска логина.
- Карта лояльности:
- тип штрих-кода: EAN-8, EAN-13, CODE-128.
- …
2.3.2 Пути решенияКак сделать качественный конфиг? Само качество определим так:
- удобство работы – «простота» чтения, «простота» заполнения (в идеале, хотим DSL);
- Скорость обработки – важно, чтобы чтение конфига не тормозило приложение.
Выделим основные пути:
- GradlebuildConfigField
- задаём переменные в gradle скрипте;
- во время компиляции генерится java класс BuildConfig, переменные доступны как его поля.
- JSON
- json объект в файле;
- зашит локально, либо получаем с сервера.
Кратко оценим пути по критериям.2.3.3 Путь №1. Gradle buildConfigFieldПлюсы:
- удобство создания — делаем DSL на минималках: выносим типы и возможные значения параметров в переменные; выявляем синтаксические ошибки на компиляции;
- простота — большинству уже знаком;
- скорость — обращаемся к классу BuildConfig в памяти.
Главный минус — отвратительная читаемость: переменная состоит из четырёх блоков, подсветки синтаксиса нет.Пример переменной на условном DSL:buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS2.3.4 Путь №2. JSONПлюсы:
- удобство чтения — особенно в формате HOCON;
- удобство создания — делаем DSL через JSON Schema, проверяем на ошибки по мере написания;
- переиспользование — шарим между iOS и Android.
Минусы:
- скорость — придётся перед запуском считать из файла или получать с сервера;
- время на освоение — по сравнению с первым вариантом JSON Schema наверняка менее популярна.
2.3.5 Так что же лучше?Когда делали проект, даже не изучали альтернативы. Сразу сделали через Gradle. На мой взгляд, JSON + Schema его побеждает. Удобство чтения — приоритет, при этом удобство создания остаётся на том же уровне, если не лучше. Дополнительная секунда для загрузки файла на общем фоне незначительна.
Сделали конфиг через Gradle, не изучая альтернатив. Но оказалось, что JSON Schema удобнее для чтения – и это её главное преимущество.
2.3.6 Best practices для buildConfigFieldЕсли выбрали buildConfigField, то в «сыром» виде с ним будут проблемы:
- чтобы использоватьEnum, придётся указать полный путь к пакету как в типе, так и в значениях;
- при изменение имени или типа переменной придётся делать Find & Replace по всем конфигам.
Решение: DSL на минималках. Заводим переменные для названий параметров, а также кастомных типов и вариантов значений. Создаём отдельный gradle-скрипт на каждый модуль. Параметры описываем в формате «экран-параметр-переменные». Скрипты кладём в папку business_rules.Пример: модуль лояльности loyalty_business_rules.gradle:
/*_______________ENTER USER ID________________*/
/*________User ID________*/
/*__Variable__*/
ext.USER_ID_VAR = "USER_ID"
ext.USER_ID_TYPE = "com.example.whitelabelexample.domain.models.UserIdType"
/*__Values__*/
ext.UI_PHONE = USER_ID_TYPE + ".PHONE"
ext.UI_EMAIL = USER_ID_TYPE + ".EMAIL"
/*_______________NO CARD________________*/
/*________Obtain card methods________*/
/*__Variable__*/
ext.OBTAIN_METHODS_VAR = "OBTAIN_CARD_METHODS"
ext.OBTAIN_METHODS_ENUM = "com.example.whitelabelexample.domain.models.ObtainCardMethod"
ext.OBTAIN_METHODS_TYPE = "java.util.List<" + OBTAIN_METHODS_ENUM + ">"
/*__Optional values__*/
ext.OM_GENERATE = OBTAIN_METHODS_ENUM + ".GENERATE_VIRTUAL"
ext.OM_BIND = OBTAIN_METHODS_ENUM + " .BIND_PHYSICAL"
...
UI_PHONE — что за UI_? Это сокращение переменной UserId: добавляем префиксы, чтобы избежать коллизий.Дальше настраиваем приложения в скриптах flavor, которые на первом шаге заботливо вытащили по файлам.Пример: flavor_loyaka.gradle:
...
loyaka {
...
/* MAIN SCREEN */
buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_CARD
/* MODULES */
buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOWCASE)
/* REGISTRATION */
buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_EMAIL
...
}
flavor_jewelry.gradle:
...
jewelry {
...
/* MAIN SCREEN */
buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS
/* MODULES */
buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOPS)
/* REGISTRATION */
buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_PHONE
...
}
2.3.7 Получаем доступ к конфигуСпроектируем решение в контексте Clean Architecture.Классы конфигов приравниваю к источникам в слое data, ибо они только предоставляют данные. Тогда ui получает параметры посредством domain.Как сгруппировать параметры по классам? Мы на проекте за группу взяли экран, а зря. С одной стороны, компактные классы, а с другой — возникли коллизии, например на экранах ввода номера телефона и подтверждения кода нужна маска номера. Пришлось реализовать считывание из конфига 2 раза.С BuildConfig это легко, но с JSON будет грязно. Считаю, что оптимально группировать по процессу (флоу). Под процессом здесь понимаю целевой use case и вторичные по отношению к нему. Обычно это группа экранов, например в модуле лояльности два целевых процесса:
- аутентификация — чтобы авторизоваться, придётся ввести логин, а затем код подтверждения;
- просмотр дисконтной карты — чтобы получить доступ, нужно сначала её привязать, либо сгенерировать.
Пример реализации конфига для второго процесса: фрагмент BuildCardConfig.kt:
class BuildCardConfig : CardConfig {
override fun numberMask(): String = BuildConfig.CARD_NUMBER_MASK
override fun barcodeType(): BarcodeType = BuildConfig.BARCODE_TYPE
override fun obtainmentMethods(): List<ObtainCardMethod> = BuildConfig.OBTAIN_CARD_METHODS
...
}
В итоге получим архитектуру работы с конфигом (диаграмма классов UML; в ui MVVM):
UseCase работает только с одним конфигом, ибо в этом классе есть все параметры целевого процесса. Если требуется больше одного — звоночек, похоже UseCase делает много вещей.2.3.8 Валидируем конфиг«Зачем нужна прослойка в domain? Она же будет пустая!» Необязательно. В идеале, хотим защиту от дурака — проверку параметров фичей на непротиворечивость. Допустим, дано 2 параметра:
- включённые модули;
- главный экран.
Если модуль «новости и акции» выключён, то логично, что главным экраном «новости» быть не может. Но на уровне Gradle или JSON Schema подобное ограничение сделать нетривиально — таким правилам и место в domain.Например, реализуем описанное условие в GetMainTabUseCase.kt:
class GetMainTabUseCase(
private val mainConfig: MainConfig
) {
operator fun invoke(): NavigationTab {
val mainTab = mainConfig.mainTab()
val mainModule = tabsByModules.entries.find { it.value == mainTab }!!.key
val isModuleEnabled = BuildConfig.APP_MODULES.contains(mainModule)
if (isModuleEnabled.not()) {
throw IllegalStateException("Can't use a tab ($mainTab) as main, it's module is disabled — fix config!")
}
return mainTab
}
}
Возникает проблема: если создавать UseCase на каждый параметр, то будет много пустых классов. Ведь правила есть не везде.Альтернатива — создавать UseCase только по надобности, но тогда возникает неоднородность: в ui используются одновременно и Config и UseCase. Рискуем использовать параметры, которые требуют валидации, в её обход, и следом за этим растёт вероятность багов.Впрочем, если правил у вас субъективно мало и коллизии очевидные, такая валидация не нужна. Мы обошлись без неё и багов из-за этого пока не ловили.2.4 Настраиваем фичи2.4.1 Выбираем модулиМы должны отключать модули, которые не нужны клиенту. Под модулем здесь пониманием связный и независимый кусок функциональности, обычно крупный. Например, модуль «лояльность»: в него входит работа с дисконтной картой и аутентификация.
Модуль здесь – крупный связный и независимый кусок функциональности.
В идеале хочется выпиливать код и ресурсы выключенных модулей из APK. Но на мой взгляд, это оверкилл, поэтому ограничимся функциональным отключением.Рассмотрим главные точки связи модуля с приложением:
- Переходы изui — боттом навигация, рандомная кнопка, etc;
- Реакция на события — кастомные (выбран город), платформы (найдена сеть), etc.
По опыту, обычно хватит убрать пункт модуля из меню, например, для лояльности — это таб из боттом навигации. Я реализовал пример в MainViewModel и MainActivity тестового проекта.Но бывает, что есть межмодульные переходы, например когда по кнопке из акции попадаем на экран карты – такое придётся обработать отдельно для каждого кейса.На события в «Лояке» реагирует только модуль пушей. Когда юзер выбирает свой город – подписываемся на соответствующий новостной канал. Опять же обрабатываем каждый кейс.Всплывает неочевидный минус buildConfigField – придётся указывать параметры даже для выключенных модулей, хотя бы как null, иначе проект не соберётся.2.4.2 Настраиваем экраны и бизнес-правилаНастройка конкретных фичей основана на уже спроектированной архитектуре. Например, при отсутствии сети мы хотим показывать карту лояльности из кэша. Однако хотим это настраивать, чтобы отключать кэш для кейсов, когда критична свежесть данных.На уровне UseCase берём параметр из нужного класса Config.GetCardUseCase.kt:
class GetCardUseCase(
private val netRep: CardNetRepository,
private val storageRep: CardStorageRepository,
private val config: CardConfig
) {
operator fun invoke(): Card? {
return if (config.isCacheCard()) {
try {
val card = netRep.getCard()
storageRep.save(card)
card
} catch (exception: Exception) {
return storageRep.get()
}
} else {
netRep.getCard()
}
}
}
В ui же обращаемся к UseCase на уровне ViewModel или Presenter.Например, получаем карту двумя способами: привязываем физическую или генерируем виртуальную. Не у всех клиентов доступны оба способа, но один гарантирован.Реализация: NoCardViewModel.kt:
class NoCardViewModel(
private val getObtainMethodsUseCase: GetObtainMethodsUseCase,
...
){
private val cardObtainMethods by lazy { getObtainMethodsUseCase() }
val isShowGetVirtualButton by lazy {
cardObtainMethods.contains(ObtainCardMethod.GENERATE_VIRTUAL)
}
val isShowBindPlasticButton by lazy {
cardObtainMethods.contains(ObtainCardMethod.BIND_PHYSICAL)
}
...
}
fragment_nocard.xml:
...
<com.google.android.material.button.MaterialButton
android:id="@+id/no_card_bind_plastic_button"
...
app:isVisible="@{viewmodel.isShowBindPlasticButton}" />
<com.google.android.material.button.MaterialButton
android:id="@+id/no_card_get_virtual_button"
...
app:isVisible="@{viewmodel.isShowGetVirtualButton}" />
...
2.4.3 Ещё один трюкИногда вариативную вёрстку целесообразнее сделать без конфига.Вспомним начало поста, хотим показать дисконтную карту – номер, статус, размер скидки и баланс бонусов. Однако процессинг лояльности у клиентов разный, поэтому не все поля гарантированы:
Аналитик собрал наиболее вероятные поля, дизайнер продумал кейсы и переиспользовал вьюхи. Получилось так, что обошлись даже без конфига — смотрим на то, какие данные приходят с бэкенда и показываем необходимое состояние. В тестовом проекте реализован пример — CardInfoFragment.3 Подведём итогМы успешно спроектировали архитектуру White Label android-проекта, которая соответствует поставленным требованиям, а именно позволяет:✅ развивать общую кодовую базу – расширять модули фичей и собирать из одного кода разные приложения, от 10 до 100;✅ создавать приложения под клиента – настраивать набор модулей и параметры фичей, менять бренд, делать это относительно быстро (до четырёх часов, по моей оценке).Горькими уроками поделились, best practices передали. Надеюсь, наш опыт создал цельное представление о создании White Label android-приложений и комфортную отправную точку для вашего проекта.Напомню, что полный код примера доступен на гитхабе — можно изучить реализацию подробнее, поиграть с настройками. Пример полноценного приложения есть в сторе.Если возникли вопросы — пишите в комменты, буду рад ответить!4 Куда развить решение?
- Мы продаём целую систему, а что если продавать модули в другие приложения? Тот же White Label, но на системный уровень ниже. Мы такую задачу решали, если интересно – напишите в комментах, расскажем.
- Когда количество приложений растёт, хочется CI и CD. В этом репозитории есть подробный гайд по настройке Azure Devops.
- Если не нужна детальная настройка фичей, а писать flavors руками надоело – сделайте автогенерацию flavors по json конфигу.
- Бизнес бьёт ключом, клиентов больше сотни? Пора автоматизировать создание приложений.
Если знаете кейсы, на которые нет ссылок в нашей статье, то обязательно скиньте их в комменты – вместе мы точно соберём крутую библиографию!P.S. Shout-out дорогим коллегам за работу над проектом и помощь в написании статьи - без вас это было бы невозможно :)
===========
Источник:
habr.com
===========
Похожие новости:
- [Информационная безопасность, Разработка под Android] Как root-права и альтернативные прошивки делают ваш android смартфон уязвимым
- [Kotlin] Из JCenter в Maven или короткая заметка о публикации мультиплатформы Kotlin
- [Разработка мобильных приложений, Законодательство в IT] МВД добавит в приложение «МВД России» модуль «Антимошенник», который будет иметь доступ к контактам пользователей
- [Разработка мобильных приложений, Тестирование мобильных приложений, Дизайн мобильных приложений] UI элементы и жесты в мобильных приложениях
- [Веб-аналитика, Интернет-маркетинг, Управление продуктом, Криптовалюты] Кейс: увеличение капитализации DeFi-стартапа на 300%
- [Разработка мобильных приложений, Здоровье] Модель адаптивного усвоения углеводов часть 2: Разложение углеводов
- [Смартфоны, IT-компании] Google ищет аналог пометкам о слежке в iOS
- [Высокая производительность, Разработка под iOS, Разработка мобильных приложений, Разработка под Android, Микросервисы] Envoy как универсальный сетевой примитив
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android] Чего ждать от коробочных приложений?
- [Высокая производительность, Программирование, Java, Конференции] JPoint и Joker: какие доклады запомнились мне больше всего
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_white_label, #_android, #_arhitektura_prilozhenij (архитектура приложений), #_kejs (кейс), #_arhitektura (архитектура), #_best_practices, #_konstruktor_prilozhenij (конструктор приложений), #_platforma_dlja_razrabotki (платформа для разработки), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:42
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Как написать код один раз, а продать 20 мобильных приложений? Мы нашли ответ путём проб и факапов и разложили опыт по пунктам: из статьи вы узнаете, как безболезненно реализовать White Label android-проект. Greetings and salutations! По работе я однажды получил крутую задачу по разработке White Label android-приложения. Изучил достижения коллег в этой области и нашёл только:
Задача: создавать приложения для разных клиентов из единой кодовой базы
Мы хотим поддерживать как можно больше программ лояльности, при этом интерфейс оставить общим. Предусмотрим вариативность, получим возможный вид экрана карты для разных клиентов: Как реализовать такой проект без боли? Прочитайте статью и найдёте ответ.1.2 Детализируем требованияРазложим видение по полочкам: как в ТЗ, но проще.Функциональные требования
… и получим четыре жирные проблемы:
Задача – одна кодовая база, до 100 приложений. Решение – Gradle Product Flavors.
apply from: "$rootDir/project_flavors/flavors_common.gradle"
android { productFlavors { loyaka { dimension APP_DIMENSION resValue "string", APP_NAME_VAR, 'Лояка' applicationId BASE_PACKAGE + 'loyaka' } } } apply from: "$rootDir/project_flavors/flavors_common.gradle"
android { productFlavors { jewerly { dimension APP_DIMENSION resValue "string", APP_NAME_VAR, 'Ювелирия' applicationId BASE_PACKAGE + 'jewelry' } } } android {
ext.DIMENSION_APP = "app" ext.APP_NAME_VAR = "app_name" ext.BASE_PACKAGE = "com.livetyping." } ...
apply from: "$rootDir/project_flavors/flavor_loyaka.gradle" apply from: "$rootDir/project_flavors/flavor_jewelry.gradle" apply from: "$rootDir/project_flavors/flavors_common.gradle" android { ... flavorDimensions APP_DIMENSION } ...
Слева — ресурсы по flavor; справа — итоговый APK.Задача решена! Уберём дефолтные ресурсы в main, а в конкретных flavor будем переопределять по необходимости.2.2.2 Best practicesКрайне важно заранее договориться с дизайнерами:
Создать и поддерживать чёткую схему для всех приложений на порядок дешевле, чем расширять код, состоящий из «уникальных» косяков.
<?xml version="1.0" encoding="utf-8"?>
<resources> <color name="active">#68b881</color> <color name="background">#36363f</color> <color name="disabled">#daede0</color> <color name="field_dark">#f5f5f5</color> ... </resources
<?xml version="1.0" encoding="utf-8"?>
<resources> <color name="active">#a160d5</color> <color name="background">#f6ebff</color> <color name="disabled">#e2c8f6</color> <color name="field_dark">#f5f5f5</color> ... </resources>
Сделали конфиг через Gradle, не изучая альтернатив. Но оказалось, что JSON Schema удобнее для чтения – и это её главное преимущество.
/*_______________ENTER USER ID________________*/
/*________User ID________*/ /*__Variable__*/ ext.USER_ID_VAR = "USER_ID" ext.USER_ID_TYPE = "com.example.whitelabelexample.domain.models.UserIdType" /*__Values__*/ ext.UI_PHONE = USER_ID_TYPE + ".PHONE" ext.UI_EMAIL = USER_ID_TYPE + ".EMAIL" /*_______________NO CARD________________*/ /*________Obtain card methods________*/ /*__Variable__*/ ext.OBTAIN_METHODS_VAR = "OBTAIN_CARD_METHODS" ext.OBTAIN_METHODS_ENUM = "com.example.whitelabelexample.domain.models.ObtainCardMethod" ext.OBTAIN_METHODS_TYPE = "java.util.List<" + OBTAIN_METHODS_ENUM + ">" /*__Optional values__*/ ext.OM_GENERATE = OBTAIN_METHODS_ENUM + ".GENERATE_VIRTUAL" ext.OM_BIND = OBTAIN_METHODS_ENUM + " .BIND_PHYSICAL" ... ...
loyaka { ... /* MAIN SCREEN */ buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_CARD /* MODULES */ buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOWCASE) /* REGISTRATION */ buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_EMAIL ... } ...
jewelry { ... /* MAIN SCREEN */ buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS /* MODULES */ buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOPS) /* REGISTRATION */ buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_PHONE ... }
class BuildCardConfig : CardConfig {
override fun numberMask(): String = BuildConfig.CARD_NUMBER_MASK override fun barcodeType(): BarcodeType = BuildConfig.BARCODE_TYPE override fun obtainmentMethods(): List<ObtainCardMethod> = BuildConfig.OBTAIN_CARD_METHODS ... } UseCase работает только с одним конфигом, ибо в этом классе есть все параметры целевого процесса. Если требуется больше одного — звоночек, похоже UseCase делает много вещей.2.3.8 Валидируем конфиг«Зачем нужна прослойка в domain? Она же будет пустая!» Необязательно. В идеале, хотим защиту от дурака — проверку параметров фичей на непротиворечивость. Допустим, дано 2 параметра:
class GetMainTabUseCase(
private val mainConfig: MainConfig ) { operator fun invoke(): NavigationTab { val mainTab = mainConfig.mainTab() val mainModule = tabsByModules.entries.find { it.value == mainTab }!!.key val isModuleEnabled = BuildConfig.APP_MODULES.contains(mainModule) if (isModuleEnabled.not()) { throw IllegalStateException("Can't use a tab ($mainTab) as main, it's module is disabled — fix config!") } return mainTab } } Модуль здесь – крупный связный и независимый кусок функциональности.
class GetCardUseCase(
private val netRep: CardNetRepository, private val storageRep: CardStorageRepository, private val config: CardConfig ) { operator fun invoke(): Card? { return if (config.isCacheCard()) { try { val card = netRep.getCard() storageRep.save(card) card } catch (exception: Exception) { return storageRep.get() } } else { netRep.getCard() } } } class NoCardViewModel(
private val getObtainMethodsUseCase: GetObtainMethodsUseCase, ... ){ private val cardObtainMethods by lazy { getObtainMethodsUseCase() } val isShowGetVirtualButton by lazy { cardObtainMethods.contains(ObtainCardMethod.GENERATE_VIRTUAL) } val isShowBindPlasticButton by lazy { cardObtainMethods.contains(ObtainCardMethod.BIND_PHYSICAL) } ... } ...
<com.google.android.material.button.MaterialButton android:id="@+id/no_card_bind_plastic_button" ... app:isVisible="@{viewmodel.isShowBindPlasticButton}" /> <com.google.android.material.button.MaterialButton android:id="@+id/no_card_get_virtual_button" ... app:isVisible="@{viewmodel.isShowGetVirtualButton}" /> ... Аналитик собрал наиболее вероятные поля, дизайнер продумал кейсы и переиспользовал вьюхи. Получилось так, что обошлись даже без конфига — смотрим на то, какие данные приходят с бэкенда и показываем необходимое состояние. В тестовом проекте реализован пример — CardInfoFragment.3 Подведём итогМы успешно спроектировали архитектуру White Label android-проекта, которая соответствует поставленным требованиям, а именно позволяет:✅ развивать общую кодовую базу – расширять модули фичей и собирать из одного кода разные приложения, от 10 до 100;✅ создавать приложения под клиента – настраивать набор модулей и параметры фичей, менять бренд, делать это относительно быстро (до четырёх часов, по моей оценке).Горькими уроками поделились, best practices передали. Надеюсь, наш опыт создал цельное представление о создании White Label android-приложений и комфортную отправную точку для вашего проекта.Напомню, что полный код примера доступен на гитхабе — можно изучить реализацию подробнее, поиграть с настройками. Пример полноценного приложения есть в сторе.Если возникли вопросы — пишите в комменты, буду рад ответить!4 Куда развить решение?
=========== Источник: habr.com =========== Похожие новости:
Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:42
Часовой пояс: UTC + 5