评论

收藏

[Android] 倒霉的菜鸟

移动开发 移动开发 发布于:2021-09-25 21:10 | 阅读数:523 | 评论:0

1, 新建项目VariantTest
2, 生成keystore
可以看到, 默认的build variant只有debug一种
DSC0000.png

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

什么错呢
DSC0002.png

大致意思是说我们的app没有签名
我们知道签名需要一个keystore, 那么作为一个个人开发者,怎么获取keystore呢?
studio给我们提供了创建keystore的方式:
DSC0003.png

DSC0004.png

DSC0005.png

DSC0006.png

DSC0007.png

DSC0008.png

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

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

以为这就大功告成了吗? 点击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
DSC00011.png

这当然是不能接受的, 因为它两长得一模一样,我完全分不清。
怎么去改app名字呢? 我们知道app名字定义在manifest中, 所以我们很容易想到新建一个manifest文件for release
DSC00012.png

只需要在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图标, 这里不再演示。
DSC00013.png

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:
DSC00014.png

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

我们也希望它们有不同的包名, 这样我可以在一台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了
DSC00016.png

但是到目前为止, cert/prod/mock的内容完全一样,根本体现不出应有的价值,那么接下来就是最关键的操作了, 怎么让不同的build去关联不同的环境呢?
我们很容易想到通过BuildConfig在代码中获取到当前的build flavors, 然后可以据此判断,设置不同的环境。如下面的代码:
DSC00017.png

当不同环境之间只有极少数区别且不涉及频繁改动的时候, 这种方式当然也可以。 但缺点是耦合性太高,不利于后期的维护和扩展
因此在项目中, 我更偏向于使用一个json文件来描述不同的配置, 比如我们之前在environment_flavors.gradle中声明了3种flavors: cert/mock/production
那么对应的,我们可以在app/src下面创建3个assets文件夹,分别放入apiConfig.json
DSC00018.png

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

定义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文件
简单测试一下,代码如下
DSC00020.png

DSC00021.png

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

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验证一下:
DSC00023.png

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
DSC00024.png

7,  按需打包
看到这里, 我们就掌握了多维app构建的基本方法,当然我们还可以增加更多的维度,比如按应用市场, baidu/huawei/xiaomi 等等, 但基本原理都是一样的。
然而,就当我准备关电脑下班时, 老板又找到了我, 提出了新需求:
在我等加班
DSC00025.png
DSC00026.png
DSC00027.png
关注下面的标签,发现更多相似文章