1, 新建项目VariantTest

2, 生成keystore

可以看到, 默认的build variant只有debug一种

当我试图选release的时候,发现报错了

什么错呢

大致意思是说我们的app没有签名

我们知道签名需要一个keystore, 那么作为一个个人开发者,怎么获取keystore呢?

studio给我们提供了创建keystore的方式:

现在我们已经有了keystore, 那么下一步就是给项目添加签名信息

加完这些以后同步一下, 我们看到已经可以build release app了

以为这就大功告成了吗? 点击installRelease,

....几秒钟之后, 我得到了一个error

Execution failed for task ':app:installRelease'.
> java.util.concurrent.ExecutionException: com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.example.varianttest signatures do not match the previously installed version; ignoring!

意思是说, 当前试图安装的应用(com.example.varianttese)的签名和之前安装的不匹配。 (因为我之前已经安装了一个debug app), 虽然这次装的是release, 但因为没改包名,所以被认为是同一个app。

这里也体现了android的应用签名机制。

那就改下包名吧:

如果我们的app只有debug和release两种, 那么完全可以在buildType/release下面声明一个不同的applicationId

但是鉴于我们后面还需要添加多个variants, 因此我们新建一个gradle文件来处理包名-

app_ids.gradle

android.applicationVariants.all { variant ->

    def buildType = variant.buildType.name
def applicationId = "com.example.varianttest" if (buildType.toLowerCase().contains("release")){
applicationId += ".release"
} variant.mergedFlavor.setApplicationId(applicationId)
}

然后, 在app/build.gradle 文件头部去引用它:
apply from: '../app_ids.gradle'

同步一下, 再点击installRelease, 很快我手机上就有了两个app-- VariantTest

这当然是不能接受的, 因为它两长得一模一样,我完全分不清。

怎么去改app名字呢? 我们知道app名字定义在manifest中, 所以我们很容易想到新建一个manifest文件for release

只需要在src下面新建release目录, 放入manifest。 完全不需要其他的配置, 编译release app时就会读取release目录目录下的manifest并和默认manifest合并。

tools: replace的作用就是告诉编译器,需要将该属性替换

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.varianttest">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name_release"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VariantTest"
tools:replace="android:label">
</application>

</manifest>

如此操作之后, 我们得到了两个名字不一样的app, 同理也可以改app图标, 这里不再演示。

3,从cert/prod的角度构建不同的app

现在为止我们得到debug/release两个app, 通常来说,debug app不做混淆, 会显示一些我们需要的log,并且可以断点调试

如果我们只是平时自己写着玩,这个buildType就够了。

但是对于绝大多数app来说,都不可避免地要使用网络和server交互。同一条请求,测试环境和生产环境要用到不同的domain,传入不同的参数。 或者有的功能我们希望只在测试app中开放

这个时候老板就希望我们能给build variants加上cert/ production两种

同时在开发过程中, app端和server端往往同步开工, 那么在api没有ready的情况下我们也希望有个mock环境能供我们调试native UI

那就开始搞吧

新建一个gradle文件-- environment_flavors.gradle: 定义了cert, prod, mock三种环境

android {
productFlavors {
cert {
dimension 'environment'
}
production {
dimension 'environment'
}
mock {
dimension 'environment'
}
}
}
然后在app/build.gradle首部添加 
apply from: '../environment_flavors.gradle'

并且申明 flavors: environment:

同步一下, 现在我们已经可以看到这些variants:

我们也希望它们有不同的包名, 这样我可以在一台device上同时安装多个variants

因此我们修改app_ids.gradle, 修改后的代码如下:(红色为本次修改的部分)

android.applicationVariants.all { variant ->

def buildType = variant.buildType.name
def applicationId = "com.example.varianttest"
def environmentName = variant.productFlavors[0].name

if (environmentName == "cert") {
applicationId += ".cert"
}
if (environmentName == "production") {
applicationId += ".prod"
}
if (environmentName == "mock") {
applicationId += ".mock"
}

if (buildType.toLowerCase().contains("release")){
applicationId += ".release"
}

variant.mergedFlavor.setApplicationId(applicationId)
}

包名不同保证了我们可以同时安装, 此外我们也希望这些app有不同的名字,否则装在一起我们完全不知道谁是谁

这时我们已经不大可能为每个variant都去创建一个manifest了, 怎么办呢?我们可以使用占位符来解决

manifest文件中:

android:label="@string/app_name${appNameEnv}${appNameBuildType}"

再修改app/build.gradle:

defauleConfig{
...
    manifestPlaceholders = [appNameEnv: "", appNameBuildType: ""]
}
buildTypes {
release {
...
manifestPlaceholders.appNameBuildType = '_release'
}
}

然后 environment_flavors.gradle:

android {
productFlavors {
cert {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_cert'
}
production {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_prod'
}
mock {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_mock'
}
}
}

同步一下, 现在我们已经可以得到6个build了

但是到目前为止, cert/prod/mock的内容完全一样,根本体现不出应有的价值,那么接下来就是最关键的操作了, 怎么让不同的build去关联不同的环境呢?

我们很容易想到通过BuildConfig在代码中获取到当前的build flavors, 然后可以据此判断,设置不同的环境。如下面的代码:

当不同环境之间只有极少数区别且不涉及频繁改动的时候, 这种方式当然也可以。 但缺点是耦合性太高,不利于后期的维护和扩展

因此在项目中, 我更偏向于使用一个json文件来描述不同的配置, 比如我们之前在environment_flavors.gradle中声明了3种flavors: cert/mock/production

那么对应的,我们可以在app/src下面创建3个assets文件夹,分别放入apiConfig.json

apiConfig.json (mock和production中host的值分别对应.mock和.production)

定义data class ApiConfiguration

data class ApiConfiguration(
val host: String
)

创建一个工具类读取assets中的json文件并转换为ApiConfiguration对象

interface AssetsLoader {

fun getApiConfiguration() : ApiConfiguration
}

class ApplicationAssetsLoader(private val configLoader: ConfigurationLoader) : AssetsLoader {

override fun getApiConfiguration(): ApiConfiguration {
return loadConfig("apiConfig.json")
}

private inline fun <reified T : Any> loadConfig(fileName: String): T {
return configLoader.requireConfig(fileName)
}
}

interface ConfigurationLoader {
fun <T : Any> loadConfig(fileName: String, type: KClass<T>): T?
}

inline fun <reified T : Any> ConfigurationLoader.requireConfig(
fileName: String
): T {
return loadConfig(fileName, T::class)
?: throw IllegalStateException("$fileName config file does not exist")
}

class JsonConfigurationLoader(
val gson: Gson,
val assets: AssetManager
) : ConfigurationLoader {

override fun <T : Any> loadConfig(fileName: String, type: KClass<T>): T? {
return try {
BufferedReader(InputStreamReader(assets.open(fileName)))
.use { reader -> gson.fromJson(reader, type.java) }
} catch (e: IOException) { // Exception is thrown if file is missing or couldn't be read
null
}
}
}

这里其实可以写得很简单, 本例中因为考虑到后面不同variants可能还要读取一些不同的文件类型, 所以抽象出了接口。

因为我们之前在app/build.gradle中已经声明了:

flavorDimensions 'environment'

所以只要上面我们新建的那三个文件夹的名字和environment_flavors.gradle中声明的一致,就不再需要其他的任何配置, 每个buildVariants都可以读到正确的json文件

简单测试一下,代码如下

本例中使用了MVVM, 数据驱动UI。分别跑一下cert/mock/prod app, 可以看到它们都拿到了正确的环境配置

4, Mock环境搭建

看到这里, 聪明的小伙伴们肯定会有个疑问, mock环境通常供开发者调试ui使用, 并不涉及和api的交互, 所以自然也就不需要api domain之类的东西

那么怎么实现mock呢?

比如,现在我们有一条网络请求getMoney,要去server拿response显示在home页面

于是我们根据api同事预先提供的返回数据格式写了数据类

data class GetMoneyResponse(
val name: String,
val count: Int,
val type: String,
val currency: String
)

接口GetMoneyRepository:

interface GetMoneyRepository {
fun getMoney(): GetMoneyResponse
}

接口实现类:

class GetMoneyRepositoryImpl() : GetMoneyRepository {
override fun getMoney(): GetMoneyResponse {
//这里应该要去call api
//本例省去了这个步骤
return GetMoneyResponse("name", 0, "type", "currency")
}
}

然后在viewModel中调用

private val _response = MutableLiveData<GetMoneyResponse>().apply {
value = GetMoneyRepositoryImpl().getMoney()
}

val response: LiveData<GetMoneyResponse> = _response

在fragment 显示

private fun getData(){
homeViewModel.response.observe(viewLifecycleOwner, {
responseView.text = it.name + "通过:" + it.type + "赚到了:"+ it.count + it.currency

})
}

至此, native部分就写完了。可是在api迟迟没有ready的情况下, 我们怎么用mock数据来测试呢?

上文中, 我们已经为mock环境创建了mock文件夹,并放入了mock build会用到的assets文件

现在我们在该文件夹下新建两个子目录with和without

将类GetMoneyRepositoryImpl移到without目录下, 我们希望真实环境(cert/prod)下可以编译这个文件

然后在with目录下再创建一个GetMoneyRepositoryImpl供mock环境使用

class GetMoneyRepositoryImpl() : GetMoneyRepository {

override fun getMoney(): GetMoneyResponse {
//因为这个类供mock使用, 因此我们可以直接返回我们想要的任何response
//通常的做法是在mock/assets下加入我们想要的response文件,如 getMoneyResponse.json, 然后读取assets
//本例中简化了这一步
return GetMoneyResponse("张三", 500, "搬砖", "人民币")
}

}
所以现在的目录就变成了这样

注意, 这里的两个实现类GetMoneyRepositoryImpl拥有完全相同的类名和包名,只是方法实现不同

因此, 我们会发现viewModel里面报错了, 因为编译器不允许同时存在两个一样的类

所以下一步,我们就需要告诉编译器,什么时候该用哪个类

在app/build.gradle 下面添加如下描述:

android {
String mockSources = "src/mock/with"
String noMockSources = "src/mock/without"
sourceSets {
main {
java.srcDirs += ['src/main/kotlin']
}
cert {
java.srcDirs += [noMockSources]
}
mock {
java.srcDirs += [mockSources]
}
production {
java.srcDirs += [noMockSources]
}
}
}

这段的作用就是告诉编译器,mock环境就编译“src/mock/with”下面的代码, 否则就编译“src/mock/without”下的代码

大功告成, 我们分别安装cert和mock app验证一下:

 5, 多维变体

就当我觉得可以松一口气的时候,老板又提出了新需求, 随着公司业务的不断扩展, 我们的app在全球范围内都有了客户群,各种风格/功能上的差异已经不仅仅是改改copy就能解决的了。所以老板希望我们能再增加一个国家的维度, 给不同的国家提供不通的app

本质上讲, 这和上文说到的environment变体并没有什么不同, 只是新增一个维度而已,  下面我们来看具体实现

新建country_flavors.gradle, 为了简单,我们只声明了china和uk两个国家

android {
productFlavors {
china {
dimension 'country'
}
uk {
dimension 'country'
}
}
}

在app/build.gradle中引用这个文件

apply from: '../country_flavors.gradle'

并修改flavorDimensions, 增加country维度

flavorDimensions 'country', 'environment'

修改app_ids.gradle,让不同的国家拥有不同的包名

android.applicationVariants.all { variant ->

    def buildType = variant.buildType.name
def countryName = variant.productFlavors[0].name.toUpperCase()
def environmentName = variant.productFlavors[1].name
def appIdCountry = AppId.valueOf(countryName)
def applicationId = appIdCountry.appId if (environmentName == "cert") {
applicationId += appIdCountry.certSuffix
}else if (environmentName == "production") {
applicationId += appIdCountry.productionSuffix
} else if (environmentName == "mock") {
applicationId += appIdCountry.mockSuffix
} if (buildType.toLowerCase().contains("release")){
applicationId += appIdCountry.RELEASE_SUFFIX
} variant.mergedFlavor.setApplicationId(applicationId)
} enum AppId {
CHINA("com.variant.china"),
UK("com.variant.uk") public final String appId
private final static String MOCK_SUFFIX = ".mock"
private final static String CERT_SUFFIX = ".cert"
public final static String RELEASE_SUFFIX = ".release"
private final static String PROD_SUFFIX = ".prod"
public final String certSuffix
public final String mockSuffix
public final String productionSuffix AppId(String appId, String certSuffix = CERT_SUFFIX, String prodSuffix = PROD_SUFFIX, String mockSuffix = MOCK_SUFFIX) {
this.appId = appId
this.certSuffix = certSuffix
this.mockSuffix = mockSuffix
this.productionSuffix = prodSuffix
}
}

同时, 在app/src目录下新建china/res/values/strings.xml :

<resources>
<string name="app_name_cert">Variant China Cert Debug</string>
<string name="app_name_cert_release">Variant China Cert Release</string>
<string name="app_name_mock">Variant China Mock Debug</string>
<string name="app_name_mock_release">Variant China Mock Release</string>
<string name="app_name_prod">Variant China Prod Debug</string>
<string name="app_name_prod_release">Variant China Prod Release</string>
<string name="title_home">主页</string>
<string name="title_dashboard">活动</string>
<string name="title_notifications">通知</string>
</resources>

  和 uk/res/values/strings.xml:

<resources>
<string name="app_name_cert">Variant UK Cert Debug</string>
<string name="app_name_cert_release">Variant UK Cert Release</string>
<string name="app_name_mock">Variant UK Mock Debug</string>
<string name="app_name_mock_release">Variant UK Mock Release</string>
<string name="app_name_prod">Variant UK Prod Debug</string>
<string name="app_name_prod_release">Variant UK Prod Release</string>
<string name="title_home">Home</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_notifications">Notifications</string>
</resources>

  这样,不同的app也可以读到不同的copy,显示不同的包名

注意这里和android 的copy 国际化不太一样, 没有根据local来确定copy, 而是根据我们自己设置的build variant, 处理更加灵活

6, 现在我们已经可以从country的维度来build出不同的app了, 那么接下来, 怎么让不同的country有不同的功能呢?

类似于上文第4步, 在app/src/china以及app/src/uk目录下新建assets文件夹, 加入featureConfig.json (China配置为true, uk配置false)

{
"showImage": true
} 

我们根据该config来决定要不要显示首页的一张图片

private fun initImageView(){
homeViewModel.showImage.observe(viewLifecycleOwner, {showImage ->
if (showImage){
imageView.visibility = View.VISIBLE
} else {
imageView.visibility = View.GONE
}
})
homeViewModel.getFeatureConfiguration(requireContext())
}

json文件的读取也与第4步相似,不再赘述。我们直接来看结果, 下图中左边是china, 右边是uk

7,  按需打包

看到这里, 我们就掌握了多维app构建的基本方法,当然我们还可以增加更多的维度,比如按应用市场, baidu/huawei/xiaomi 等等, 但基本原理都是一样的。

然而,就当我准备关电脑下班时, 老板又找到了我, 提出了新需求:

在我等加班几年的努力下, 我们的app功能不断增多, 引入了大量的第三方库, 导致的结果就是app size不断增大, 眼看就要突破google设置的150M生死线, 所以给app瘦身就成了当前迫在眉睫的问题。

经粗略统计, 我们共引入了几十个第三方库, 但是并非所有的app都需要这些库, 所以我们应该通过country来配置依赖

这里我们以 okhttp为例, 假设china 需要okhttp在首页加载一张图片, 但uk全程都不需要

那我们先新建country_implementations.gradle文件, 并在app/build.gradle中引用

dependencies {
chinaImplementation "com.squareup.okhttp3:okhttp:4.4.0"
chinaImplementation "com.squareup.okhttp3:okhttp-urlconnection:4.4.0"
}

注意这里的 chinaImplementation  意思就是只给china 添加依赖。

不用担心编译器找不到这个方法, 因为我们之前已经声明了名为 country的flavor, 包括了china和uk

所以编译器完全可以识别这个命令, 就跟我们平常用的testImplementation, debugImplementation一样

接下来就是怎么调用的问题了, 以前我们直接在整个工程下添加依赖, 这样项目里的任何地方都可以获取到该依赖

但现在,因为我们只给china 加了,所以不能在工程代码里直接调用。否则,当你build uk app时根本找不到

比如,当我build uk variant时, 这行代码是报错的

怎么解决呢?

其实思路和上文中搭建mock环境一样

在src下新建journey/okhttp/main/java目录, 分别放入两个同名的工具类HttpJourney

在with/main/java目录下的文件里, 我们实现了我们要用到的http journey的一些方法

在without/main/java目录下的文件里, 只需要定义空方法,或者直接throw exception

然后在fragment里调用

private fun initImageView(){
homeViewModel.showImage.observe(viewLifecycleOwner, {showImage ->
if (showImage){
HttpJourney().getImageViaHttp()
imageView.visibility = View.VISIBLE
} else {
imageView.visibility = View.GONE
}
})
homeViewModel.getFeatureConfiguration(requireContext())
} 

注意这里 showImage 在uk config里的配置一定是false .

代码部分加完了, 最后一步就是告诉编译器, china和uk分别要编译哪些journey的代码

我们继续在country_implementations.gradle中添加如下代码

enum Journey {
HTTP_JOURNEY("okhttp") public final String sourceSetName Journey(String sourceSetName) {
this.sourceSetName = sourceSetName
}
} static def addJourneySources(String country, List<Journey> journeys, sourceSets) {
def included = Journey.values().toList().intersect(journeys)
def excluded = Journey.values() - included
def sourceSet = sourceSets.findByName(country) for (journey in included) {
sourceSet.java.srcDirs += "src/journey/${journey.sourceSetName}/with/main/java"
}
for (journey in excluded) {
sourceSet.java.srcDirs += "src/journey/${journey.sourceSetName}/without/main/java"
}
} android {
sourceSets { container ->
china {
ArrayList journeys = new ArrayList([Journey.HTTP_JOURNEY])
addJourneySources(name, journeys, container)
}
uk {
ArrayList journeys = new ArrayList([])
addJourneySources(name, journeys, container)
}
}
}  

代码很简单,相信大家都能看懂, 这里就不废话了

看看结果,分别build和china和uk的cert app

umm, 效果还是有的

												

android-- 按需打包的框架搭建--新手教程的更多相关文章

  1. Ionic框架搭建简明教程

    1.安装node.js 安装教程:https://www.cnblogs.com/zhouyu2017/p/6485265.html 安装完成后,执行:cnpm install –g cordova ...

  2. SSM(Spring+SpringMVC+Mybatis)框架搭建详细教程【附源代码Demo】

    [前言] 应某网络友人邀约,需要一个SSM框架的Demo作为基础学习资料,于是乎,就有了本文.一个从零开始的SSM框架Demo对一个新手来说,是非常重要的,可大大减少在学习过程中遇到的各种各样的坑,说 ...

  3. 基于webpack的react开发环境搭建新手教程

    最近学习react-webpack项目搭建,找到一篇我认为不错的博客,跟着学习了一番,写得很详细很好,本篇博客纯属记录总结,要看更详细的搭建过程及解析,请戳: 基于webpack的React项目搭建( ...

  4. 【SSM框架】Spring + Springmvc + Mybatis 基本框架搭建集成教程

    本文将讲解SSM框架的基本搭建集成,并有一个简单demo案例 说明:1.本文暂未使用maven集成,jar包需要手动导入. 2.本文为基础教程,大神切勿见笑. 3.如果对您学习有帮助,欢迎各种转载,注 ...

  5. Yii框架学习 新手教程(一)

    本人小菜鸟一仅仅,为了自我学习和交流PHP(jquery,linux,lamp,shell,javascript,server)等一系列的知识,小菜鸟创建了一个群.希望光临本博客的人能够进来交流.寻求 ...

  6. SSM框架搭建最新教程(超详细)

    个人认为使用框架并不是很难,关键要理解其思想,这对于我们提高编程水平很有帮助.不过,如果用都不会,谈思想就变成纸上谈兵了!!!先技术,再思想.实践出真知. 1.基本概念 1.1.Spring  Spr ...

  7. [转]SSM(Spring+SpringMVC+Mybatis)框架搭建详细教程【附源代码Demo】

    一.新建项目 运行IDEA,进入初始化界面,然后我们选择新建项目(进入主界面新建项目也是一样的) 在Maven选项卡里面找到对应的java web选项,然后我们点下一步 这一步填入组织等信息,这里比较 ...

  8. Android基础新手教程——1.2 开发环境搭建

    Android基础新手教程--1.2 开发环境搭建 标签: Android基础新手教程 如今主流的Android开发环境有: ①Eclipse + ADT + SDK ②Android Studio ...

  9. 使用 Jenkins 搭建 iOS/Android 持续集成打包平台【转】

    背景描述 根据项目需求,现要在团队内部搭建一个统一的打包平台,实现对iOS和Android项目的打包.而且为了方便团队内部的测试包分发,希望在打包完成后能生成一个二维码,体验用户(产品.运营.测试等人 ...

随机推荐

  1. 搞懂Redis协议RESP

    RESP (REdis Serialization Protocal) Redis客户端和服务端之间通信的协议.它很简单,建立在TCP协议上,提供简单.高性能.可读性强的数据序列化的规范和语义. 5种 ...

  2. linux高级监控atop的使用

    一.centos安装 sudo yum -y install epel-release.noarch sudo yum -y install atop sudo systemctl enable at ...

  3. Dynamics CRM实体系列之1:N、N:1以及N:N关系

    Dynamics CRM在实施过程中会遇到很多多个实体关联的问题,这样可以实现多个实体的记录通过关联的字段实现数据的综合展示,在Sql Server里面叫做外键,在Dynamics CRM叫做关系.D ...

  4. vue+vant实现购物车的全选和反选业务,带你研究购物车的那些细节!

    前言 喜欢购物的小伙伴看过来,你们期待已久的购物车来啦!相信小伙伴逛淘宝时最擅长的就是加入购物车了,那购物车是如何实现商品全选反选的呢?今天就带你们研究购物车的源码,以vue+vant为例. 正文 首 ...

  5. vue3.0入门(一)

    前言 最近在b站上学习了飞哥的vue教程 学习案例已上传,下载地址 使用方式 使用在线cdn 下载js文件并自托管,引入到项目后使用 使用npm安装后,用cli来构建项目 声明式渲染 Vue2需引入v ...

  6. idea无法使用中文输入法输入

    问题--idea无法使用中文输入 原因:idea本身版本过高,所以需要你强制减低它的jdk版本 解决:使用配置idea环境变量解决 ps:目前适用于任何版本的jdk和idea 步骤: 1.新建一个ID ...

  7. Linux 安装 Harbor 私有镜像仓库

    下载 最新发行:https://github.com/goharbor/harbor/releases # 下载文件 wget https://github.com/goharbor/harbor/r ...

  8. 面试官:Redis的事务满足原子性吗?

    原创:码农参上(微信公众号ID:CODER_SANJYOU),欢迎分享,转载请保留出处. 谈起数据库的事务来,估计很多同学的第一反应都是ACID,而排在ACID中首位的A原子性,要求一个事务中的所有操 ...

  9. DOM对象入门

    1.概念 2.script最好是放在后面,等html的文档内容加载完毕,不然获取不到 3.事件基本操作 第一种绑定事件html和js耦合度高,用第二种 4.灯开关事件使用

  10. Java XXE漏洞典型场景分析

    本文首发于oppo安全应急响应中心: https://mp.weixin.qq.com/s?__biz=MzUyNzc4Mzk3MQ==&mid=2247485488&idx=1&am ...