¿Cómo escribir un código una vez y vender 20 aplicaciones móviles? Encontramos la respuesta a través de pruebas y fakups y descomponemos la experiencia en puntos: del artículo aprenderá cómo implementar sin dolor un proyecto de Android White Label.
¡Saludos y saludos! Mi nombre es Kirill, en el trabajo una vez tuve la tarea de desarrollar una aplicación para Android White Label. Estudié los logros de colegas en esta área y encontré solo:
, , , best practices. .
1
« » 10 eCommerce retail. , , : .
( ), : , .
... ... ! White Label ? : , – .
:
1.1
, . : .
, SEPHORA , «» . :
, . , :
? .
, , «» — White Label , . , — :)
1.2
: , .
:
– ;
– , , ;
...
:
, ;
, : , , .
;
– ;
10 100 .
1.3 ? ? 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
.
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."
}
, 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 . , :
! main
, flavor .
2.2.2 Best practices
:
— ;
—
colors.xml
flavor .
, , , . , , . — primary
accent
. , .
, 100 ! , , . , : , — , .
, , «» .
2.2.3
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
<?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);
– , .
:
Gradle
buildConfigField
gradle ;
java
BuildConfig
, .
JSON
json ;
, .
.
2.3.3 №1. Gradle buildConfigField
:
— DSL : ; ;
— ;
—
BuildConfig
.
— : , .
DSL:
buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS
2.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, .
...
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
...
}
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
.
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
.
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
.
, : . , .
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}" />
...
2.4.3
.
, – , , . , :
, . , — , . — CardInfoFragment.
3
White Label android-, , :
✅ – , 10 100;
✅ – , , ( , ).
, best practices . , White Label android- .
— , ! «» , :)
4 ?
, ? White Label, . , – , .
-
, flavors – flavors json .
-
, , – !
PD: Saludos a Dmitry Alekseenkov por una gran contribución al desarrollo de la aplicación de Android, Valeria Vasilyeva por la edición sensible, Valeria Panakova por las ilustraciones vívidas y el estudio de mecanografía en vivo y el equipo de Loyaki en general por hacer posible este artículo :)