新建http库工程,仅包含如下一个类NetManager.java
http/src/main/java/com.tobenull.http.NetManager.java
package com.tobenull.http;
import android.util.Log;
/**
* Created by tobenull on 11/27/17.
*/
public class NetManager {
private static NetManager instance = new NetManager();
private NetManager(){}
public static NetManager getInstance() {
return instance;
}
public void init() {
Log.d("null-tobe", "NetManager->init");
}
}
修改各个库之间的依赖关系,如下
sdk->base->http
sdk/build.gradle
...
android {
...
}
dependencies {
if (rootProject.ext.debug) {
compile project(':base')
} else {
embedded project(':base')
embedded project(':http')
}
...
}
base/build.gradle
...
dependencies {
...
compile project(':http')
}
在命令窗口输入以下命令
gradle clean main
解压sdk/build/outputs/aar目录下的sdk-alibaba-release.aar和sdk-baidu-release.aar并反编译其classes.jar,均如下:
v com.tobenull
v base
> BaseManager.class
> BuildConfig.class
> TLog.class
v http
> BuildConfig.class
> NetManager.class
v sdk
> BaseSDK.class
> BuildConfig.class
> SDK.class
而且两个渠道的SDK.class中内容分别为alibaba和baidu两个渠道的源码。
经过以上的步骤,我们只是新增了一个http库并添加了依赖配置,并没有修改合并脚本相关的任何代码,最终构建得到了符合预期的sdk(将新增的http库也合并进来),由此可见,我们的合并脚本是没问题的。
修改sdk/build.gradle脚本,新增”tencent”渠道,如下
sdk/build.gradle
...
android {
...
productFlavors {
baidu{}
alibaba{}
tencent{}
}
...
}
...
在命令窗口执行gradle clean main命令后,展开sdk/outputs/aar/目录
v sdk
v build
...
v outputs
v aar
...
sdk-alibaba-debug.aar
sdk-alibaba-release.aar
sdk-baidu-debug.aar
sdk-baidu-release.aar
sdk-tencent-debug.aar
sdk-tencent-release.aar
...
解压sdk-tencent-release.aar包并反编译其中的classes.jar,如下
v com.tobenull
v base
> BaseManager.class
> BuildConfig.class
> TLog.class
v http
> BuildConfig.class
> NetManager.class
v sdk
> BaseSDK.class
> BuildConfig.class
因为我们没有在sdk库中新建tencent的渠道,所以打出来的sdk中只有main的内容,如果有兴趣,可以试着仿照baidu或alibaba渠道新建tencent渠道并实现SDK类供第三方调用。
经过上述步骤,我们在没有修改打版脚本的情况下只是新建了一个tencent渠道,执行构建命令就成功地生成了tencent渠道的aar包,说明我们的合并脚本是没有问题的。
经过前面的文章,我们已经完成了sdk多渠道开发与构建的工作,sdk的调试我们会在app工程中也以多渠道的方式进行。设置{rootProject}/build.gradle中的debug为true、采用正常的库工程依赖方式,当调试没问题时上传代码,服务端设置debug为false进行sdk的多渠道构建。
在将{rootProject}/build.gradle中的debug设为true后,同步后会发现App.java中会报SDK找不到的提示,这是因为我们app工程只是依赖于sdk,没有设置对各个渠道的分别依赖,而SDK.java在sdk库的main中又不存在,需要对多个渠道进行单独依赖,如下:
app/build.gradle
dependencies {
if (rootProject.ext.debug) {
// compile project(':sdk')
baiduCompile project(path: ':sdk', configuration: 'baiduRelease')
alibabaCompile project(path: ':sdk', configuration: 'alibabaRelease')
tencentCompile project(path: ':sdk', configuration: 'tencentRelease')
} else {
...
}
...
}
在com.android.tools.build:gradle 3.0.0以前的版本,依赖默认使用的都是Compile,而多渠道中可以针对特定的渠道设置特定的依赖,如此处的baiduCompile等。
由于sdk中有多个不同的渠道,所以在进行依赖时需要指定configuration,这样当我们切换app工程中的渠道时、其依赖的sdk也会自动切换到对应的渠道上来。
此时如果报以下错:
Error:Project :app declares a dependency from configuration 'alibabaCompile' to configuration 'alibabaRelease' which is not declared in the descriptor for project :sdk.
需要确保sdk库中配置多渠道的位置设置publishNonDefault为true
sdk/build.gradle
android {
...
publishNonDefault true
productFlavors {
...
}
...
}
同步成功后,我们就可以针对不同的渠道通过app工程的调用对各渠道sdk进行调试了。
打开android studio的Build Variants窗口,默认显示如下:
module | Build Variant
app | alibabaDebug
base | debug
http | debug
sdk | alibabaRlease
运行app,打开日志过滤”null-“,demo运行日志如下:
12-16 14:46:14.238 10331-10331/com.tobenull.multiflavorsdk D/null-tobe: BaseSDK->init, alibaba
12-16 14:46:14.238 10331-10331/com.tobenull.multiflavorsdk D/null-tobe: BaseManager->init
12-16 14:46:14.238 10331-10331/com.tobenull.multiflavorsdk D/null-tobe: SDK->initAlibaba, init alibaba...
12-16 14:46:14.238 10331-10331/com.tobenull.multiflavorsdk D/null-tobe: MapManager->init, init gaode map...
点击”alibabaDebug”,切换为”baiduDebug”,重新运行app,日志如下:
12-16 14:49:40.882 11903-11903/? D/null-tobe: BaseSDK->init, baidu
12-16 14:49:40.882 11903-11903/? D/null-tobe: BaseManager->init
12-16 14:49:40.882 11903-11903/? D/null-tobe: SDK->initBaidu, init baidu...
根据上述日志的变化,即可发现demo依赖的渠道已由alibaba变为了baidu.从而,我们可以使用app工程对多个渠道sdk进行调试,当调试通过后,上传库工程源码进行打版、生成可用的aar包。
此外,我们也需要对app进行多渠道配置,main目录存放公共代码、各渠道中存放对应渠道的测试代码。这样一方面可以避免app测试工程在来回切换渠道时由于各渠道提供接口的不一致出现混乱,另一方面我们后续可以根据各渠道和main中的测试源码自动生成demo.
为app工程建立多渠道包
鼠标选中app/src目录、右键->New->Folder->Java Folder,点击Target Source Set下的main,选择alibaba,finish,即完成了alibaba渠道的创建。使用同样的方式建立baidu、tencent渠道。
使用mvp模式,在main中定义统一的接口及view,在各渠道中各自实现相应的presenter.
代码如下:
app/src/main/java/com/tobenull/multiflavorsdk/App.java
package com.tobenull.multiflavorsdk;
import android.app.Application;
import com.tobenull.sdk.SDK;
/**
* Created by tobenull on 11/25/17.
*/
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
SDK.getInstance().init();
}
}
app/src/main/java/com/tobenull/multiflavorsdk/interfaces/IMainView.java
package com.tobenull.multiflavorsdk.interfaces;
/**
* Created by tobenull on 12/16/17.
*/
public interface IMainView {
void showResult(String result);
}
app/src/main/java/com/tobenull/multiflavorsdk/interfaces/IMainPresenter.java
package com.tobenull.multiflavorsdk.interfaces;
import android.widget.BaseAdapter;
/**
* Created by tobenull on 12/16/17.
*/
public interface IMainPresenter {
BaseAdapter getAdapter();
void onItemClick(int position);
}
app/src/main/java/com/tobenull/multiflavorsdk/MainActivity.java
package com.tobenull.multiflavorsdk;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import com.tobenull.multiflavorsdk.interfaces.IMainPresenter;
import com.tobenull.multiflavorsdk.interfaces.IMainView;
public class MainActivity extends AppCompatActivity implements IMainView {
private IMainPresenter mainPresenter;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainPresenter = new MainPresentImpl(this, this);
textView = (TextView) findViewById(R.id.textview);
ListView listView = (ListView) findViewById(R.id.listview);
listView.setAdapter(mainPresenter.getAdapter());
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
mainPresenter.onItemClick(position);
}
});
}
@Override
public void showResult(String result) {
textView.setText(result);
}
}
MainActivity布局如下:
app/src/main/res/layout/activity_main.xml
<LinearLayout
xmlns:android = "http://schemas.android.com/apk/res/android"
xmlns:tools = "http://schemas.android.com/tools"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
android:orientation="vertical"
tools:context = "com.tobenull.multiflavorsdk.MainActivity">
<ListView
android:id = "@+id/listview"
android:layout_width = "match_parent"
android:layout_height = "0dp"
android:layout_weight = "1"
tools:ignore = "Suspicious0dp" />
<ScrollView
android:layout_width = "match_parent"
android:layout_height = "0dp"
android:layout_weight = "1"
tools:ignore = "Suspicious0dp">
<TextView
android:id = "@+id/textview"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:layout_centerInParent = "true"
android:text = "Hello World!" />
ScrollView>
LinearLayout>
渠道实现如下:
app/src/alibaba/java/com/tobenull/multiflavorsdk/MainPresentImpl.java
package com.tobenull.multiflavorsdk;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Toast;
import com.tobenull.multiflavorsdk.interfaces.IMainPresenter;
import com.tobenull.multiflavorsdk.interfaces.IMainView;
import com.tobenull.sdk.SDK;
/**
* Created by tobenull on 12/16/17.
*/
public class MainPresentImpl implements IMainPresenter {
private Context context;
private IMainView mainView;
private BaseAdapter adapter;
public MainPresentImpl(Context context, IMainView mainView) {
this.context = context;
this.mainView = mainView;
if (adapter == null)
adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[] {
"SDK.getInstance().init();",
"SDK.getInstance().buy(product);",
});
}
@Override
public BaseAdapter getAdapter() {
return adapter;
}
@Override
public void onItemClick(int position) {
StringBuffer result = new StringBuffer();
switch (position) {
case 0:
SDK.getInstance().init();
result.append("调用了SDK.getInstance().init();方法");
break;
case 1:
String product = "alibaba debug demo";
SDK.getInstance().buy(product);
result.append("调用了SDK.getInstance().buy(product);方法\nproduct = " + product);
break;
}
mainView.showResult(result.toString());
}
}
运行demo后,依次点击init和buy的测试方法,日志如下:
12-16 02:55:35.189 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: BaseSDK->init, alibaba
12-16 02:55:35.189 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: BaseManager->init
12-16 02:55:35.189 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: SDK->initAlibaba, init alibaba...
12-16 02:55:35.189 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: MapManager->init, init gaode map...
12-16 02:55:36.271 2309-2309/com.tobenull.multiflavorsdk D/null-tobe: SDK->search, you want to buy alibaba debug demo
同样的,在Build Variant窗口中切换渠道为baiduDebug,实现IMainPresenter接口如下:
app/src/baidu/java/com/tobenull/multiflavorsdk/MainPresenterImpl.java
package com.tobenull.multiflavorsdk;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Toast;
import com.tobenull.multiflavorsdk.interfaces.IMainPresenter;
import com.tobenull.multiflavorsdk.interfaces.IMainView;
import com.tobenull.sdk.SDK;
/**
* Created by tobenull on 12/16/17.
*/
public class MainPresentImpl implements IMainPresenter {
private Context context;
private IMainView mainView;
private BaseAdapter adapter;
public MainPresentImpl(Context context, IMainView mainView) {
this.context = context;
this.mainView = mainView;
if (adapter == null)
adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[] {
"SDK.getInstance().init();",
"SDK.getInstance().search(key);",
});
}
@Override
public BaseAdapter getAdapter() {
return adapter;
}
@Override
public void onItemClick(int position) {
StringBuffer result = new StringBuffer();
switch (position) {
case 0:
SDK.getInstance().init();
result.append("调用了SDK.getInstance().init();方法");
break;
case 1:
String key = "baidu debug demo";
SDK.getInstance().search(key);
result.append("调用了SDK.getInstance().search(key);方法\nkey = " + key);
break;
}
mainView.showResult(result.toString());
}
}
运行app,日志如下:
12-16 03:03:49.400 8923-8923/com.tobenull.multiflavorsdk D/null-tobe: BaseSDK->init, baidu
12-16 03:03:49.400 8923-8923/com.tobenull.multiflavorsdk D/null-tobe: BaseManager->init
12-16 03:03:49.400 8923-8923/com.tobenull.multiflavorsdk D/null-tobe: SDK->initBaidu, init baidu...
12-16 03:03:50.970 8923-8923/com.tobenull.multiflavorsdk D/null-tobe: SDK->search, you want to search baidu debug demo
经过上述步骤,我们现在已经实现了在同一个app工程下对多个渠道sdk进行调试。当调试无误后,即可上传库工程源码进行打版。
经过前面的介绍,我们现在已经可以得心应手地进行多渠道sdk的构建、开发和调试了。现在如果需要新建一个渠道,我们需要在sdk和app工程中分别进行配置,然后分别新建渠道代码,如果能通过一处进行统一管理就堪称完美了。
构建脚本中一共有三处配置需要灵活处理,如下:
sdk/build.gralde
android {
...
productFlavors {
baidu{}
alibaba{}
tencent{}
}
...
}
app/build.gradle
android {
...
productFlavors {
baidu{
applicationId 'com.baidu.demo'
}
alibaba{
applicationId 'com.alibaba.demo'
}
tencent{
applicationId 'com.tencent.demo'
}
}
...
}
app/build.gradle
dependencies {
...
baiduCompile project(path: ':sdk', configuration: 'baiduRelease')
alibabaCompile project(path: ':sdk', configuration: 'alibabaRelease')
tencentCompile project(path: ':sdk', configuration: 'tencentRelease')
...
}
我们可以将渠道包信息在{rootProject}/build.gradle中统一定义,然后三处分别动态添加即可。
首先在rootProject中定义一组配置信息,每个配置均有product、packageName和type三个属性,如下:
build.gradle
buildscript {
ext {
...
SDKProperties = [
[product: 'alibaba', packageName: 'com.alibaba.demo', type: 'jar'],
[product: 'baidu', packageName: 'com.baidu.demo', type: 'aarjar'],
[product: 'tencent', packageName: 'com.tencent.demo', type: 'aar'],
]
...
}
...
}
以上属性中product作为渠道的标志,packageName为该渠道对应demo的包名,type属性暂时不用、作为后续生成jar、aar或jar+aar三种sdk产品的标记。
动态添加渠道
sdk/build.gradle
android {
...
productFlavors {
rootProject.ext.SDKProperties.each {
productFlavors.create(it.product)
}
}
...
}
app/build.gradle
android {
...
productFlavors {
rootProject.ext.SDKProperties.each {
productFlavors.create(it.product).setApplicationId(it.packageName)
}
}
...
}
通过rootProject.ext.SDKProperties.each遍历SDKProperties中配置的渠道包信息,productFlavors.create()动态添加渠道包,app工程中创建好渠道包后通过setApplicationId()即可设置该渠道对应demo的包名。
注:此处的it代表的是SDKProperties中的每一项配置([product: ‘xxx’, packageName: ‘com.xxx.demo’, type: ‘xxx’])
动态配置依赖
app/build.gradle
dependencies {
if (rootProject.ext.debug) {
rootProject.ext.SDKProperties.each {
println "configurations: ${it.product}Compile"
dependencies.add("${it.product}Compile", project(
path: ':sdk', configuration: "${it.product}Release")
)
}
} else {
...
}
...
}
与前文写法类似,gradle可以通过”${it.product}Compile”动态生成alibabaCompile、baiduCompile等字符串然后使用dependencies.add()的方式添加相应的依赖
经过上面的处理,我们可以只通过{rootProject}/build.gradle中的SDKProperties对渠道包进行管理。比如,现在需要新增一个product为iqiyi、包名为com.iqiyi.demo的渠道,则只需在SDKProperties中新增一行即可。
如果iqiyi和baidu渠道类似,我们还可以通过脚本自动复制baidu的渠道代码、然后再次基础上进行进一步的开发,类似以下的处理方式:
sdk/build.gradle
rootProject.ext.SDKProperties.each {
def product = it.product
if (it.packageName == "" || it.packageName.length() <= 0)
return
// 如果渠道对应的sdk源码包不存在,则从base复制一份,根据base进行修改
if (!file("src/$product").exists()) {
copy {
from "src/base"
into "src/$product"
}
}
// 如果渠道对应的demo源码包不存在,则从base复制一份,根据base进行修改
if (!file("../app/src/$product").exists()) {
copy {
from "../app/src/base"
into "../app/src/$product"
}
}
}
在sdk开发、特别是多渠道sdk开发时,最好建立一个通用的base渠道,通过以上方式会在gradle同步时自动复制base渠道的源码到新建的渠道、新渠道也可以在添加后立马编译执行。
此处我们以baidu渠道作为base渠道进行演示。
新建iqiyi渠道,如下:
build.gradle
buildscript {
ext {
...
SDKProperties = [
...
[product: 'iqiyi', packageName: 'com.iqiyi.demo', type: 'aar'],
]
...
}
...
}
同步gradle成功后,在Build Variants窗口将渠道切换为iqiyiDebug,运行demo,日志如下:
12-16 04:57:54.581 32194-32194/? D/null-tobe: BaseSDK->init, baidu
12-16 04:57:54.581 32194-32194/? D/null-tobe: BaseManager->init
12-16 04:57:54.589 32194-32194/? D/null-tobe: SDK->initBaidu, init baidu...
由此,我们实现了通过一行配置信息自动管理多渠道包的配置。
本文首先分别通过新增库依赖和新增渠道、在不改变构建脚本的情况下,生成了符合要求的sdk包,验证了前文sdk多渠道构建脚本的正确性。
接着对多渠道调试进行了探究,通过配置多渠道app工程对多渠道sdk进行了本地调试、通过Build Variants窗口进行各渠道之间的切换。
最后对多个渠道的配置进行了优化,通过动态添加渠道信息、动态添加依赖、动态复制源码的方式,使得多渠道的开发、调试与管理变得更加简单,只需一行配置信息即可进行控制。
至此,关于多渠道sdk的开发、构建与调试心得的介绍也就基本结束了。后续将着重介绍自动生成demo及文档等内容,使得sdk的开发更加标准化,不用再费力地管理demo和文档了。