1. 前言
当一个APP成熟起来,功能会越来越多,业务会越来越复杂,面向的用户群越来越大。这个时候为了更进一步的发展和扩大业务,我们可以对APP进行拆分,做成两个甚至更多个的APP,每个APP都服务特定的用户群。为了缩短开发时间,降低维护成本,肯定是不能再单独新建一个项目工程的。那怎么在原有的项目工程来进行开发,从而实现一套代码能够打出不同的APP呢?这就是本篇文章要介绍的“差异化打包”。
2. 解决方案
2.1 原理
本篇只介绍使用Gradle来进行打包的方式,其他打包方式就不介绍了(因为我就只会这种,哈哈哈)。在 Android Studio 升级到3.0之后,Gradle 增加了多维度管理配置 FlavorDimensions 和 ProductFlavors,详细介绍可见这篇文章 “Android : 代码多维度管理(产品风味) ”。而我们就是利用这个多维度管理配置来进行差异化打包。
2.2 需求
在这里我将用我的一个开源项目 BlogDemo 进行演示。先来说一下具体的需求:
- 将APP拆分成两个相互独立的APP,一个是简易版,即原来的APP,另一个是专业版,即新的APP
- 简易版APP的名称为“BlogDemo”,包名为 com.fantasy.blogdemo,服务器地址为 https://fantasy.com
- 专业版APP的名称为“BlogDemoPro”,包名为 com.fantasy.blogdemo.pro,服务器地址为 https://fantasy.pro.com
- 简易版APP在欢迎页使用 Android 自带的 Dialog 来提示内容,专业版APP在欢迎页使用第三方UI库 QMUI 的 Dialog 来提示内容
需求就这几个,接下来就开始编码吧!
2.3 编码
在 app 模块的 build.gradle 中,我们可以在 android 这个闭包里面加上如下配置,就可以实现两个APP使用不同的APP名称、包名和服务器地址。
android {
// 差异化打包配置
flavorDimensions "mode"
productFlavors {
simple { // 简易版
applicationId "com.fantasy.blogdemo"
resValue "string", "app_name", "BlogDemo"
buildConfigField "String", "SERVER_URL", "\"https://fantasy.com\""
}
profession { // 专业版
applicationId "com.fantasy.blogdemo.pro"
resValue "string", "app_name", "BlogDemoPro"
buildConfigField "String", "SERVER_URL", "\"https://fantasy.pro.com\""
}
}
}
温馨提示:在这里已经配置了 app_name,那 strings.xml 文件中的 app_name 就要删了,不然会报错。
接下来来实现这一需求:简易版APP在欢迎页使用Android自带的Dialog来提示内容,专业版APP在欢迎页使用第三方UI库 QMUI 的Dialog来提示内容。
首先在 app 模块的 build.gradle 中,添加以下依赖:
professionImplementation 'com.qmuiteam:qmui:1.2.0'
一般我们添加依赖使用 implementation 或 api 就可以了,但是为了避免影响到简易版APP,这就要求只有在编译专业版APP的时候,才能依赖 QMUI,所以这里才在前面加了 profession,这个是专业版APP的差异化打包的风味标识,这样做就可以满足需求了。如果是简易版的话,我们就用 simple 这个。
因为两个APP的欢迎页有点不一样,所以我们要做两个欢迎页。首先我们要在 app模块的 src 里创建两个文件夹 simple 和 profession。
如上图所示,main 文件中的代码和资源是公共的,即简易版和专业版都有的功能。simple 文件夹就是放简易版APP特有的文件,而profession 文件夹就是放专业版APP特有的文件。
先来讲讲专业版APP,在java文件夹中,我们在 com.fantasy.blogdemo.main 目录下建立 WelcomeActivity.java,详细代码如下:
package com.fantasy.blogdemo.main;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import com.fantasy.blogdemo.BuildConfig;
import com.fantasy.blogdemo.R;
import com.fantasy.blogdemo.base.BaseActivity;
import com.qmuiteam.qmui.widget.dialog.QMUIDialog;
import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction;
/**
* 主界面(专业版)
* <pre>
* author : Fantasy
* version : 1.0, 2020-05-30
* since : 1.0, 2020-05-30
* </pre>
*/
public class WelcomeActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 解决Android应用Launcher重复启动的问题
if (!isTaskRoot() && getIntent() != null) {
String action = getIntent().getAction();
if (getIntent().hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) {
finish();
return;
}
}
setContentView(R.layout.activity_welcome);
// 全屏,透明状态栏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
View decorView = getWindow().getDecorView();
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
new QMUIDialog.MessageDialogBuilder(this)
.setMessage("服务器地址为:" + BuildConfig.SERVER_URL)
.addAction(R.string.btn_confirm, new QMUIDialogAction.ActionListener() {
@Override
public void onClick(QMUIDialog dialog, int index) {
dialog.dismiss();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
MainActivity.actionStart(WelcomeActivity.this);
finish();
}
}, 1500);
}
})
.show();
}
}
因为专业版会单独使用 QMUI,又因为 QMUI 需要设置 theme,所以我们在 profession 的 res 文件夹中,加入values/styles.xml文件,在里面配置 AppTheme 的style,而 main 里 values/styles.xml 文件中的 AppTheme style 就要移除。profession 的 AppTheme 具体如下:
<resources>
<!-- profession 专业版的主题 -->
<style name="AppTheme" parent="QMUI.Compat.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
接下来就是简易版APP,相同的,在 simple 文件夹中,我们在 java/com.fantasy.blogdemo.main 目录下建立 WelcomeActivity.java,在 res/values 文件夹中加入styles.xml。具体代码如下:
package com.fantasy.blogdemo.main;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Toast;
import com.fantasy.blogdemo.BuildConfig;
import com.fantasy.blogdemo.R;
import com.fantasy.blogdemo.base.BaseActivity;
/**
* 主界面(简易版)
* <pre>
* author : Fantasy
* version : 1.1, 2020-05-30
* since : 1.0, 2019-06-05
* </pre>
*/
public class WelcomeActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 解决Android应用Launcher重复启动的问题
if (!isTaskRoot() && getIntent() != null) {
String action = getIntent().getAction();
if (getIntent().hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) {
finish();
return;
}
}
setContentView(R.layout.activity_welcome);
// 全屏,透明状态栏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
View decorView = getWindow().getDecorView();
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
// 因为简易版没有依赖 QMUI,所以无法使用 QMUIDialog
Toast.makeText(this, "服务器地址为:" + BuildConfig.SERVER_URL, Toast.LENGTH_LONG).show();
// new QMUIDialog.MessageDialogBuilder(this)
// .setMessage("服务器地址为:" + BuildConfig.SERVER_URL)
// .addAction(R.string.btn_confirm, new QMUIDialogAction.ActionListener() {
// @Override
// public void onClick(QMUIDialog dialog, int index) {
// dialog.dismiss();
// }
// })
// .show();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
MainActivity.actionStart(WelcomeActivity.this);
finish();
}
}, 1500);
}
}
<resources>
<!-- simple 简易版的主题 -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
最后我们在 main 目录里的 AndroidManifest.xml 中,加入简易版APP和专业版APP共有的配置:
<activity android:name=".main.WelcomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
因为现在有2套产品风味,那我们在平常开发中,要如何调测呢?其实很简单,我们只要在 build variant 进行切换就可以了。
为了能够区分两个不同APP的安装包,我们可以在 app 模块的 build.gradle 中的 android 闭包里面增加下列配置:
android {
// 自定义安装包的名称,这里使用了 productFlavors 的名称,这样简易版和专业版的安装包也就可以区分出来了
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "BLOG_V${variant.versionName}_${variant.versionCode}_${getTime()}" +
"_${variant.productFlavors[0].name}.apk"
}
}
}
打包的时候,我们只要选择两个产品风味各自的release就行了。
2.4 演示
把打好的两个APP安装到手机里面,安装好后,可以看到手机桌面会有两个APP(如下图所示),其中 BlogDemo 就是简易版APP,而 BlogDemoPro 就是专业版APP。
分别打开一下,各自的欢迎页效果如下所示:
以上所演示的功能的相关代码已提交到 BlogDemo,欢迎查阅!