Una guía del sistema para crear aplicaciones de Android de marca blanca

¿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:





  • (, , , etc) , ;





  • , (, , etc).





, , , best practices. .





1

« » 10 eCommerce retail. , , : .





( ), : , .





... ... ! White Label ? : , – .





:





1.1

, . : .





, SEPHORA , «» . :





, . , :





? .





, , «» — White Label , . , — :)





1.2

: , .





  1. :





    • – ;





    • – , , ;





    • ...





  2. :





    • , ;





    • , : , , .





  • ;





  • – ;





  • 10 100 .





1.3 ? ? White Label?

– . , « ». , . :





  1. /:





    • ;





    • , AppGyver – drag’n’drop , ( , );





    • – .





  2. White Label:





    • , ;





    • (, )





. , White Label. «white label android development» , .





2

White Label

«» «» ( , ). , Clean Architecture…





… :





  1. ?





  2. ?





  3. ?





  4. ?





, , !





2.1

– , 100 . – Gradle Product Flavors.





Gradle Product Flavors, . White Label:





, «» . , main



.





. , .





. 100, . , .





, , : , .





flavors. , :





  1. «» — ;





  2. «» — .





flavors — loyaka



jewelry



. best practice — flavor . ? .





:





  1. project_flavors



    ;





  2. — gradle- flavor_loyaka.gradle



    , flavor_jewelry.gradle



    flavors_common.gradle



    ;





  3. 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 :





  1. main



    ;





  2. gradle main



    flavor;





  3. flavor . , main/res



    , loyaka



    loyaka/res



    ;





, main/res



loyaka/res



animal.webp



? , , Gradle . , :





Izquierda - recursos por sabor;  a la derecha está el APK final.
— 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

:





  1. ;





  2. .





, . : . .





«--»:





  1. :





    • ;





    • ;









  2. :





    • : email;





    • .





  3. :





    • -: EAN-8, EAN-13, CODE-128.









2.3.2

? :





  1. – «» , «» ( , DSL);





  2. – , .





:





  1. Gradle buildConfigField







    • gradle ;





    • java BuildConfig



      , .





  2. 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



, «» :





  1. Enum



    , , ;





  2. 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 . , :





  1. — , , ;





  2. — , , .





: 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 :





  1. ;





  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. , , .





:





  1. ui



    — , , etc;





  2. — ( ), ( ), 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 .





  • , ? .





, , – !





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 :)








All Articles