Bagaimana cara menulis kode sekali dan menjual 20 aplikasi seluler? Kami menemukan jawabannya melalui uji coba dan fakups dan menguraikan pengalaman tersebut menjadi poin-poin: dari artikel tersebut Anda akan belajar cara menerapkan proyek android Label Putih tanpa rasa sakit.
Salam dan salam! Nama saya Kirill, di tempat kerja saya pernah mendapat tugas keren untuk mengembangkan aplikasi android White Label. Saya mempelajari prestasi kolega di bidang ini dan hanya menemukan:
, , , 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 .
-
, , – !
PS Shout-out to Dmitry Alekseenkov atas kontribusinya yang sangat besar untuk pengembangan aplikasi android, Valeria Vasilyeva untuk pengeditan sensitif, Valeria Panakova untuk ilustrasi yang hidup, dan studio Live Typing dan tim Loyaki secara umum yang membuat artikel ini mungkin terjadi :)