# Android 常见问题
# 1. 集成时常见问题
# 1.1 集成SDK要求的最低环境配置是什么?
答:集成SDK要求Android SDK版本为19(Android 4.4)及以上。
# 1.2 具体的集成步骤是怎样的?
答:请参考 SDK集成文档
# 1.3 集成SDK是否需要配置混淆规则?
答:需要配置,混淆规则如下:
-keep class com.finogeeks.** {*;}
# 1.4 初始化SDK的时候,有哪些参数是必须配置的?
答:初始化SDK时至少需要传入SDK Key、SDK Secret、服务器地址、服务器接口请求路由前缀、加密方式,通过FinAppConfig
实例传入配置参数。
2.13.102
版本之前,SDK仅支持配置一个服务器信息,只能打开单个环境中的小程序,如下:
FinAppConfig config = new FinAppConfig.Builder()
.setSdkKey(BuildConfig.SDK_KEY) // SDK Key,增加合作应用成功后由平台签发
.setSdkSecret(BuildConfig.APP_SECRET) // SDK Secret,增加合作应用成功后由平台签发
.setApiUrl(BuildConfig.API_URL) // 服务器地址
.setApiPrefix(BuildConfig.API_PREFIX) // 服务器接口请求路由前缀
.setEncryptionType(ENCRYPTION_TYPE_SM) // 加密方式,国密:SM,md5: MD5
.build();
从2.13.102
版本开始支持配置多个服务器信息,可以同时打开不同环境中的小程序,如下:
// 服务器信息集合
List<FinStoreConfig> storeConfigs = new ArrayList<>();
// 服务器1的信息
FinStoreConfig storeConfig1 = new FinStoreConfig(
"SDK Key信息", // SDK Key
"SDK Secret信息", // SDK Secret
"服务器1的地址", // 服务器地址
"服务器1的数据上报服务器地址", // 数据上报服务器地址
"/api/v1/mop/", // 服务器接口请求路由前缀
"",
"加密方式" // 加密方式,国密:SM,md5: MD5
);
storeConfigs.add(storeConfig1);
// 服务器2的信息
FinStoreConfig storeConfig2 = new FinStoreConfig(
"SDK Key信息", // SDK Key
"SDK Secret信息", // SDK Secret
"服务器2的地址", // 服务器地址
"服务器2的数据上报服务器地址", // 数据上报服务器地址
"/api/v1/mop/", // 服务器接口请求路由前缀
"",
"加密方式" // 加密方式,国密:SM,md5: MD5
);
storeConfigs.add(storeConfig2);
FinAppConfig config = new FinAppConfig.Builder()
.setFinStoreConfigs(storeConfigs) // 服务器信息集合
.build();
# 1.5 初始化SDK的时候,有哪些参数是选择性配置的?
答:除了上面提到的必须配置的参数,其它参数都是根据需要自行选择配置的,主要包括UI配置、灰度发布规则配置。
UI配置如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否隐藏右上角关闭按钮
uiConfig.setHideNavigationBarCloseButton(false);
// 是否隐藏"更多"菜单中的"转发"按钮
uiConfig.setHideForwardMenu(false);
// 是否隐藏更多菜单中的"反馈与投诉"
uiConfig.setHideFeedbackAndComplaints(false);
// 是否隐藏导航栏中的"返回首页"按钮
uiConfig.setHideBackHome(false);
// 当导航栏为默认导航栏时,是否始终显示返回按钮
uiConfig.setAlwaysShowBackInDefaultNavigationBar(false);
// 导航栏标题文字样式
uiConfig.setNavigationBarTitleTextAppearance(R.style.TextAppearance_AppCompat);
// 导航栏标题相对父控件的Gravity
uiConfig.setNavigationBarTitleTextLayoutGravity(Gravity.CENTER);
// 是否清除导航栏导航按钮的背景
uiConfig.setClearNavigationBarNavButtonBackground(true);
// "更多"菜单样式
uiConfig.setMoreMenuStyle(UIConfig.MORE_MENU_DEFAULT);
// 胶囊按钮配置
uiConfig.setCapsuleConfig(new CapsuleConfig());
APM数据上报扩展信息如下:
// APM数据上报扩展信息
Map<String, Object> apmExtendInfo = new HashMap<>();
apmExtendInfo.put("key1", "value1");
apmExtendInfo.put("key2", "value2");
可选参数通过FinAppConfig
实例和必配参数一起传给SDK,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否隐藏右上角关闭按钮
uiConfig.setHideNavigationBarCloseButton(false);
// 是否隐藏"更多"菜单中的"转发"按钮
uiConfig.setHideForwardMenu(false);
// 是否隐藏"更多"菜单中的"设置"按钮
uiConfig.setHideSettingMenu(false);
// 是否隐藏更多菜单中的"反馈与投诉"
uiConfig.setHideFeedbackAndComplaints(false);
// 是否隐藏导航栏中的"返回首页"按钮
uiConfig.setHideBackHome(false);
// 当导航栏为默认导航栏时,是否始终显示返回按钮
uiConfig.setAlwaysShowBackInDefaultNavigationBar(false);
// 导航栏标题文字样式
uiConfig.setNavigationBarTitleTextAppearance(R.style.TextAppearance_AppCompat);
// 导航栏标题相对父控件的Gravity
uiConfig.setNavigationBarTitleTextLayoutGravity(Gravity.CENTER);
// 是否清除导航栏导航按钮的背景
uiConfig.setClearNavigationBarNavButtonBackground(true);
// "更多"菜单样式
uiConfig.setMoreMenuStyle(UIConfig.MORE_MENU_DEFAULT);
// 胶囊按钮配置
uiConfig.setCapsuleConfig(new CapsuleConfig());
// APM数据上报扩展信息
Map<String, Object> apmExtendInfo = new HashMap<>();
apmExtendInfo.put("key1", "value1");
apmExtendInfo.put("key2", "value2");
// 需要移除Cookies的域名
List<String> needToRemoveCookiesDomains = new ArrayList<>();
needToRemoveCookiesDomains.add("https://aaa.bbb.ccc");
FinAppConfig config = new FinAppConfig.Builder()
.setSdkKey(BuildConfig.SDK_KEY) // SDK Key,增加合作应用成功后由平台签发
.setSdkSecret(BuildConfig.APP_SECRET) // SDK Secret,增加合作应用成功后由平台签发
.setApiUrl(BuildConfig.API_URL) // 服务器地址
.setApiPrefix(BuildConfig.API_PREFIX) // 服务器接口请求路由前缀
.setDebugMode(BuildConfig.DEBUG) // 应用当前是否是Debug模式
.setUiConfig(uiConfig) // UI配置
.setApmExtendInfo(apmExtendInfo) // APM数据上报扩展信息
.setEncryptionType(ENCRYPTION_TYPE_SM) // 加密方式,国密:SM,md5: MD5
.setDisableRequestPermissions(true) // 否禁止发起运行时权限申请,默认不禁止
.setAppletAutoAuthorize(false) // 是否开启自动授予小程序Scope权限,默认不开启
.setNeedToRemoveCookiesDomains(needToRemoveCookiesDomains) // 需要移除Cookies的域名
.setDisableTbs(true) // 是否禁止禁止启用Tbs SDK,默认不禁止
.setCustomWebViewUserAgent("aaa/111; bbb") // 自定义WebView UserAgent
.setAppletIntervalUpdateLimit(6) // 定时批量更新小程序的数量
.setForegroundServiceConfig(new FinAppConfig.ForegroundServiceConfig(true, R.drawable.ic_launcher, "小程序正在运行", "")) // 是否在小程序前台运行时启动前台服务
.setBindAppletWithMainProcess(true) // 小程序与app进程绑定,App被杀死,小程序是否同步关闭,默认为false
.setWebViewMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW) // 设置WebView mixed content mode
.setAppletText("X应用") // 替换“小程序”文案,把SDK中的“小程序”文案替换为“X应用”
.setEnableApmDataCompression(true) // 上报数据时是否对数据进行压缩,默认不压缩
.setDisableGetSuperviseInfo(true) // 是否禁止调用获取监管信息的小程序API,默认不禁止
.setUserId("用户ID") // 设置用户ID
.build();
# 1.6 初始化SDK的时候,有没有需要特别注意的地方?
答:SDK采用多进程机制实现,每个小程序运行在独立的进程中,即一个小程序对应一个进程,在初始化SDK时,要特别注意的一点是:小程序进程在创建的时候不需要执行任何初始化操作,即使是小程序SDK的初始化,也不需要在小程序进程中执行。
例如:应用使用了一些第三方库,这些库需要在应用启动时先初始化,那么,在Application
中执行初始化时,只有当前进程为宿主进程时才需要初始化这些第三方库,小程序进程是不需要初始化这些库的。
因此,在初始化SDK之前,一定要判断当前进程是哪一个进程,如果是小程序进程,并且不需要处理跨进程调用接口或是在小程序进程中注册api,就不进行任何操作了:
/**
* 应用的{@link android.app.Application}
*/
public class AppletApplication extends MultiDexApplication {
@Override
public void onCreate() {
super.onCreate();
// 判断是否为小程序进程的代码放在最前面
if (FinAppClient.INSTANCE.isFinAppProcess(this)) {
return;
}
// 其它代码放在后面
…………………………………………
…………………………………………
}
}
# 1.7 集成时遇到依赖冲突问题如何解决?
答:Android 小程序SDK 依赖的第三方库
// appcompat-v7
implementation "com.android.support:appcompat-v7:23.0.0"
// support-v4
implementation "com.android.support:support-v4:23.0.0"
// Recyclerview
implementation "com.android.support:recyclerview-v7:23.2.0"
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.61"
// Gson
implementation "com.google.code.gson:gson:2.2.2"
// zxing
implementation "com.google.zxing:core:3.3.0"
implementation "com.google.zxing:android-core:3.3.0"
// SdkCore
implementation "com.finogeeks.finochat.sdk:sdkcore:2.15.1"
sdk的依赖使用gradle管理,当app与sdk依赖了相同的库时,gradle会自动处理冲突,使用版本号较高的库。
如果app里通过jar包或源码依赖的库与sdk发生冲突,这时可以通过exclude命令剔除某个sdk的依赖。
例如:
implementation('com.finogeeks.lib:finapplet:2.21.1') {
exclude group: "com.tecent.smtt"
}
另外,还是建议使用gradle管理依赖比较好
# 1.7.1 遇到zxing:android-core里的类重复怎么办?
sdk使用了zxing:android-core库, 因为zxing-android-embedded库与android-core里的CameraConfigurationUtils类名重复,所以一些用户在使用zxing-android-embedded库后会出现类名冲突的问题。 这时可以通过exclude命令去除依赖
例如:
implementation('com.finogeeks.lib:finapplet:2.21.1') {
exclude group: "com.google.zxing" , module:"android-core"
}
# 2. 使用时常见问题
# 2.1 怎么启动小程序?
- 如果启动小程序时不携带启动参数,则通过调用
IAppletApiManager
接口的startApplet(context: Context, appId: String)
方法启动,如下:
FinAppClient.INSTANCE.getAppletApiManager().startApplet(context, "appId");
- 如果启动小程序时携带启动参数,则通过调用
IAppletApiManager
接口的startApplet(context: Context, appId: String, startParams: Map<String, String>)
方法启动,如下:
Map<String, String> params = new HashMap<>();
// path为小程序页面路径
params.put("path", "/pages/index/index");
// query为启动参数,内容为"key1=value1&key2=value2 ..."的形式
params.put("query", "aaa=test&bbb=123");
FinAppClient.INSTANCE.getAppletApiManager().startApplet(this, "appId", params);
# 2.2 在宿主应用中启动多个小程序之后,为什么在Android系统的最近任务栏列表里面除了宿主应用外,还会看到多个小程序任务?
答:因为Android小程序SDK采用多进程机制实现,宿主应用是一个进程,而每个小程序也分属一个独立的进程,在Android系统的最近任务栏中,不同的进程会分别展示出来,因此会看到多个被打开的小程序。
当下主流的小程序,例如Android版的微信小程序、Android版的百度智能小程序亦如此。
# 2.3 小程序接口是否支持扩展?即除了SDK内部提供的一系列标准接口之外,能否让小程序调用自己提供的接口?如果可以要怎么实现?
答:小程序接口支持扩展。SDK允许注册自定义的小程序接口,注册之后的自定义接口和SDK内部的标准接口一样,可以供小程序调用。
具体实现步骤如下:
- 实现自定义小程序接口。
IApi
声明了小程序接口需要实现的两个最核心的方法,即apis();
和invoke(String event, JSONObject param, ICallback callback);
,具体代码如下:
/**
* 小程序API接口,实现相应功能的API需实现此接口
*/
public interface IApi extends ILifecycle {
/**
* @return 可调用API的名称的数组
*/
String[] apis();
/**
* 接收到API调用时,会触发此方法,在此方法中实现具体的业务逻辑
*
* @param event 事件名称,即API名称
* @param param 参数
* @param callback 回调接口
*/
void invoke(String event, JSONObject param, ICallback callback);
}
实现自定义接口需要创建一个类,并让这个类继承IApi
的子类AbsApi
,然后重写apis()
方法和invoke()
方法。apis()
返回所有可调用API的名称的数组,通过实现apis()
声明所有需要实现的API。
invoke()
则会在小程序调用API时被触发,其中的参数event是API的名称,param是和API对应的参数,callback则用于执行完调用逻辑后给小程序回调数据。
例如,我们自定义一个小程序接口,用于实现简单的页面跳转功能,代码如下:
/**
* 自定义小程序接口,实现一个简单的页面跳转功能
*/
public class ApiOpenPage extends AbsApi {
private static final String API_NAME_OPEN_PAGE = "openPage";
private Context mContext;
public ApiOpenPage(Context context) {
mContext = context;
}
/**
* @return 可调用API的名称的数组
*/
@Override
public String[] apis() {
return new String[]{API_NAME_OPEN_PAGE};
}
/**
* 接收到API调用时,会触发此方法,在此方法中实现具体的业务逻辑
*
* @param event 事件名称,即API的名称
* @param param 参数
* @param callback 回调接口
*/
@Override
public void invoke(String event, JSONObject param, ICallback callback) {
if (API_NAME_OPEN_PAGE.equals(event)) {
String url = param.optString("url");
if (!TextUtils.isEmpty(url)) {
Intent intent = new Intent();
intent.setClass(mContext, SecondActivity.class);
if (!(mContext instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
mContext.startActivity(intent);
callback.onSuccess(null);
} else {
callback.onFail();
}
}
}
}
- 注册自定义小程序接口。
通过调用IExtensionApiManager
接口的registerApi
方法实现注册。如下:
FinAppClient.INSTANCE.getExtensionApiManager().registerApi(new ApiOpenPage(context));
如果需要一次注册多个接口,则可以调用registerApis
,传入的参数为接口集合。
- 在小程序工程中增加扩展接口配置。SDK注册自定义小程序接口后,还需要在小程序工程的根目录创建
FinClipConf.js
文件,在FinClipConf.js
中配置对应的自定义接口。配置示例如下:
module.exports = {
extApi:[
{
name: 'openPage', // 扩展接口名
params: { // 扩展接口参数,可以只列必须的参数
url: '',
title: '',
description: ''
}
}
]
}
- 在小程序中调用已注册接口中的各个API。
# 2.4 小程序加载网页时,网页可以调用原生的代码吗?
答:可以在小程序的网页中调用原生代码。JSSDK提供了一系列接口,通过这些接口可以实现在网页里面调用原生代码。
# 2.5 JSSDK接口是否支持扩展?即除了JSSDK内部提供的一系列标准接口之外,能否让小程序在网页中调用自己提供的接口?如果可以要怎么实现?
答:JSSDK接口支持扩展。SDK允许注册自定义的JSSDK接口,注册之后的自定义JSSDK接口和JSSDK内部提供的标准接口一样,可以供小程序在网页中调用。
具体实现步骤如下:
- 实现自定义JSSDK接口。
这一步和实现自定义小程序接口是一样的,通过继承AbsApi
,并重写apis()
方法和invoke()
方法实现自定义JSSDK接口。 - 注册自定义JSSDK接口。
通过调用IExtensionWebApiManager
接口的registerApi
方法实现注册。如下:
FinAppClient.INSTANCE.getExtensionWebApiManager().registerApi(new ApiOpenPage(context));
如果需要一次注册多个接口,则可以调用registerApis
,传入的参数为接口集合。
详细可以查看注册小程序 WebView 组件API。
# 2.6 在自定义接口的invoke()
方法中跳转到宿主App的其它页面,做完一系列操作之后,按系统返回键想返回小程序,结果却返回到了宿主App中启动小程序的页面,为什么?
原因:
跳转到宿主App其它页面这一步,是通过宿主App中的Context实例来启动Activity的,并且没有把Activity压入新的任务栈中。
Android小程序SDK是多进程架构的,小程序和宿主App处于不同进程中,所处的任务栈自然也是不同的。小程序跳转到宿主App的页面,新打开的页面是添加到宿主App原有的任务栈中的,当从页面返回时,执行的逻辑是在原生App中原有的任务栈中弹出页面,因此会看到原生App的页面被逐个关闭,最后返回到原生应用启动小程序的页面,并没有返回小程序。
解决方案:
方案1(推荐):
通过ICallback
的startActivity
或startActivityForResult
来跳转到宿主App的其它页面。这是推荐的方案,因为这样做是在小程序所在的任务栈打开新宿主App的Activity的,Activity的入栈出栈都是在同一个任务栈中完成的,没有任务栈切换的过程。
更重要的一个原因是:如果需要通过startActivityForResult来启动Activity并在页面返回时获取到回传的数据,只有使用这种方案,自定义接口的
onActivityResult
才会执行,才能拿到返回的数据。此方案使用示例:
@Override public void invoke(String event, JSONObject param, ICallback callback) { Intent intent = new Intent(); intent.setClass(mContext, SecondActivity.class); callback.startActivityForResult(intent, 100); }
方案二(不推荐):
如果一定要使用宿主App中的Context实例来启动Activity,就需要对启动原生页面的Intent
设置"支持多任务栈"和“开启新任务栈”的Flag,这样可以在原生App的进程中新开一个任务栈,开启新任务栈之后,新打开的页面将被逐个压入这个新任务栈中,当结束完原生页面的所有操作之后逐个页面返回时,便会从这个新任务栈中将页面逐个弹出,当这个新任务栈中的所有页面都被弹出后,便会回到小程序进程的任务栈。因此,在自定义接口的
invoke()
方法中,如果需要跳转到原生应用的其它页面执行某些操作,并期望当关闭这些原生页面后能够返回小程序,那么建议在执行跳转的时候为Intent
对象同时增加Intent.FLAG_ACTIVITY_MULTIPLE_TASK
和FLAG_ACTIVITY_NEW_TASK
,如下:
Intent intent = new Intent();
intent.setClass(context, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent); // context是宿主App中的Context实例
使用此方案,如果通过startActivityForResult来启动Activity,当页面返回时,自定义接口的onActivityResult
不会被调用,因此不推荐。
# 2.7 在原生代码中可以调用网页中的JS函数吗?
答:SDK支持原生代码调用js函数。通过调用IAppletApiManager
接口的callJS
方法即可实现调用,如下:
JSONObject funcParams = new JSONObject();
try {
funcParams.put("param1", "value1");
funcParams.put("param2", "value2");
FinAppClient.INSTANCE.getAppletApiManager().callJS(
"appId",
"funcName",
funcParams.toString(),
-1,
new FinCallback<String>() {
@Override
public void onSuccess(String result) {
Log.d(TAG, "callJS onSuccess : " + result);
}
@Override
public void onError(int code, String error) {
Log.d(TAG, "callJS onError : " + code + ", " + error);
}
@Override
public void onProgress(int status, String info) {
}
});
} catch (JSONException e) {
e.printStackTrace();
}
# 2.8 怎么设置小程序中Activity的转场动画?
答:通过调用IAppletApiManager
接口的setActivityTransitionAnim
方法设置小程序中Activity的转场动画,如下:
FinAppClient.INSTANCE.getAppletApiManager().setActivityTransitionAnim(SlideFromRightToLeftAnim.INSTANCE);
目前提供了五种动画可供设置:
- NoneAnim:无动画;
- SlideFromLeftToRightAnim:滑动动画-左进右出;
- SlideFromRightToLeftAnim:滑动动画-右进左出;
- SlideFromTopToBottomAnim:滑动动画-上进下出;
- SlideFromBottomToTopAnim:滑动动画-下进上出。
# 2.9 怎么分享小程序到微信等支持小程序的平台?
答:要实现小程序分享功能,总体思路是先获取到分享小程序所需要的相关信息,然后把获取到的信息转换为分享接口的参数,最后再调用分享接口把小程序分享到对应平台。具体实现方案主要有两种:
实现小程序抽象业务回调接口
IAppletHandler
的shareAppMessage方
法,并将IAppletHandler
实例传入SDK。当点击小程序更多菜单中的“转发”时,会调用
IAppletHandler
实例的shareAppMessage
方法,shareAppMessage
方法中有小程序信息、小程序页面截图等参数,获取到小程序相关参数之后,便可调用第三方分享SDK实现分享。shareAppMessage
方法如下:/** * 转发小程序 * * @param appInfo 小程序信息,是一串json,包含了小程序id、小程序名称、小程序图标、用户id、转发的数据内容等信息。 * [appInfo]的内容格式如下: * { * "appTitle": "“人民网+”小程序", * "appAvatar": "https:\/\/www.finogeeks.club\/statics\/images\/swan_mini\/swan_logo.png", * "appId": "5df36b3f687c5c00013e9fd1", * "appType": "trial", * "userId": "finogeeks", * "cryptInfo": "SFODj9IW1ENO8OA0El8P79aMuxB1DJvfKenZd7hrnemVCNcJ+Uj9PzkRkf/Pu5nMz0cGjj0Ne4fcchBRCmJO+As0XFqMrOclsqrXaogsaUPq2jJKCCao03vI8rkHilrWxSDdzopz1ifJCgFC9d6v29m9jU29wTxlHsQUtKsk/wz0BROa+aDGWh0rKvUEPgo8mB+40/zZFNsRZ0PjsQsi7GdLg8p4igKyRYtRgOxUq37wgDU4Ymn/yeXvOv7KrzUT", * "params": { * "title": "apt-test-tweet-接口测试发布的动态!@#¥%……&*(", * "desc": "您身边的服务专家", * "imageUrl": "finfile:\/\/tmp_fc15edd8-2ff6-4c54-9ee9-fe5ee034033d1576550313667.png", * "path": "pages\/tweet\/tweet-detail.html?fcid=%40staff_staff1%3A000000.finogeeks.com&timelineId=db0c2098-031e-41c4-b9c6-87a5bbcf681d&shareId=3dfa2f78-19fc-42fc-b3a9-4779a6dac654", * "appInfo": { * "weixin": { * "path": "\/studio\/pages\/tweet\/tweet-detail", * "query": { * "fcid": "@staff_staff1:000000.finogeeks.com", * "timelineId": "db0c2098-031e-41c4-b9c6-87a5bbcf681d" * } * } * } * } * } * [appInfo]中各字段的说明: * appId 小程序ID * appTitle 小程序名称 * appAvatar 小程序头像 * appType 小程序类型,其中trial表示体验版,temporary表示临时版,review表示审核版,release表示线上版,development表示开发版 * userId 用户ID * cryptInfo 小程序加密信息 * params 附带的其它参数,由小程序自己透传 * * @param bitmap 小程序封面图片。如果[appInfo].params.imageUrl字段为http、https的链接地址,那么小程序封面图片 * 就取[appInfo].params.imageUrl对应的图片,否则小程序的封面图片取[bitmap]。 * @param callback 转发小程序结果回调。 */ fun shareAppMessage(appInfo: String, bitmap: Bitmap?, callback: IAppletCallback)
通过调用
IAppletApiManager
的setAppletHandler(appletHandler: IAppletHandler)
方法传入IAppletHandler
实例,如下:FinAppClient.INSTANCE.getAppletApiManager().setAppletHandler(new IAppletHandler() { @Override public void shareAppMessage(@NotNull String appInfo, @org.jetbrains.annotations.Nullable Bitmap bitmap, @NotNull IAppletCallback callback) { // 实现分享小程序的逻辑 …………………………………………………… …………………………………………………… } });
通过自定义接口来实现。在自定义接口的
invoke
方法中接收小程序传递过来的参数,然后调用第三方分享SDK实现小程序分享。
# 2.10 如何往“更多”菜单中注入自己的菜单项?
答:和通过抽象业务回调接口IAppletHandler
实现小程序分享一样, “更多”菜单中菜单项的注入也是通过IAppletHandler
来实现的,IAppletHandler
会把获取注入菜单项的接口方法getRegisteredMoreMenuItems
和点击注入菜单项的接口方法onRegisteredMoreMenuItemClicked
回调给宿主应用,由宿主应用实现具体的业务逻辑。
getRegisteredMoreMenuItems
和onRegisteredMoreMenuItemClicked
如下:
/**
* 获取注册的"更多"菜单项
*
* @param appId 小程序ID
* @return 注册的"更多"菜单项
*/
fun getRegisteredMoreMenuItems(appId: String): List<MoreMenuItem>?
/**
* 注册的"更多"菜单项被点击
*
* @param appId 小程序ID
* @param path 小程序页面路径
* @param menuItemId 被点击的菜单条目的ID
* @param appInfo 小程序信息,是一串json,包含了小程序id、小程序名称、小程序图标、用户id、转发的数据内容等信息。
* [appInfo]的内容格式如下:
* {
* "appTitle": "“人民网+”小程序",
* "appAvatar": "https:\/\/www.finogeeks.club\/statics\/images\/swan_mini\/swan_logo.png",
* "appId": "5df36b3f687c5c00013e9fd1",
* "appType": "trial",
* "userId": "finogeeks",
* "cryptInfo": "SFODj9IW1ENO8OA0El8P79aMuxB1DJvfKenZd7hrnemVCNcJ+Uj9PzkRkf/Pu5nMz0cGjj0Ne4fcchBRCmJO+As0XFqMrOclsqrXaogsaUPq2jJKCCao03vI8rkHilrWxSDdzopz1ifJCgFC9d6v29m9jU29wTxlHsQUtKsk/wz0BROa+aDGWh0rKvUEPgo8mB+40/zZFNsRZ0PjsQsi7GdLg8p4igKyRYtRgOxUq37wgDU4Ymn/yeXvOv7KrzUT",
* "params": {
* "title": "apt-test-tweet-接口测试发布的动态!@#¥%……&*(",
* "desc": "您身边的服务专家",
* "imageUrl": "finfile:\/\/tmp_fc15edd8-2ff6-4c54-9ee9-fe5ee034033d1576550313667.png",
* "path": "pages\/tweet\/tweet-detail.html?fcid=%40staff_staff1%3A000000.finogeeks.com&timelineId=db0c2098-031e-41c4-b9c6-87a5bbcf681d&shareId=3dfa2f78-19fc-42fc-b3a9-4779a6dac654",
* "appInfo": {
* "weixin": {
* "path": "\/studio\/pages\/tweet\/tweet-detail",
* "query": {
* "fcid": "@staff_staff1:000000.finogeeks.com",
* "timelineId": "db0c2098-031e-41c4-b9c6-87a5bbcf681d"
* }
* }
* }
* }
* }
* [appInfo]中各字段的说明:
* appId 小程序ID
* appTitle 小程序名称
* appAvatar 小程序头像
* appType 小程序类型,其中trial表示体验版,temporary表示临时版,review表示审核版,release表示线上版,development表示开发版
* userId 用户ID
* cryptInfo 小程序加密信息
* params 附带的其它参数,由小程序自己透传
*
* @param bitmap 小程序封面图片。如果[appInfo].params.imageUrl字段为http、https的链接地址,那么小程序封面图片
* 就取[appInfo].params.imageUrl对应的图片,否则小程序的封面图片取[bitmap]。
* @param callback 结果回调。
*/
fun onRegisteredMoreMenuItemClicked(appId: String, path: String, menuItemId: String, appInfo: String?, bitmap: Bitmap?, callback: IAppletCallback)
同样,IAppletHandler
实例需要通过调用IAppletApiManager
的setAppletHandler(appletHandler: IAppletHandler)
方法传入。
getRegisteredMoreMenuItems
方法和onRegisteredMoreMenuItemClicked
方法实现示例如下:
/**
* {@link IAppletHandler}实现类,用于实现一些业务场景,例如注册"更多"菜单项,转发小程序等。
*/
public class AppletHandler implements IAppletHandler {
@NonNull
private Context mContext;
private AppletHandler() {
}
public AppletHandler(@NonNull Context context) {
this.mContext = context;
}
@Nullable
@Override
public List<MoreMenuItem> getRegisteredMoreMenuItems(@NotNull String appId) {
List<MoreMenuItem> items = new ArrayList<>();
MoreMenuItem item0 = new MoreMenuItem("WXShareAPPFriends", "微信好朋友", MoreMenuType.ON_MINI_PROGRAM);
items.add(item0);
MoreMenuItem item1 = new MoreMenuItem("WXShareAPPMoments", "微信朋友圈", MoreMenuType.ON_MINI_PROGRAM, true);
items.add(item1);
MoreMenuItem item2 = new MoreMenuItem("ShareSinaWeibo", "新浪微博", MoreMenuType.ON_MINI_PROGRAM);
items.add(item2);
MoreMenuItem item3 = new MoreMenuItem("ShareQQFriends", "QQ", MoreMenuType.ON_MINI_PROGRAM);
items.add(item3);
MoreMenuItem item4 = new MoreMenuItem("ShareDingDing", "Dingding", MoreMenuType.ON_MINI_PROGRAM);
items.add(item4);
MoreMenuItem item5 = new MoreMenuItem("ShareLinks", "标题以后端配置为准", MoreMenuType.ON_MINI_PROGRAM);
items.add(item5);
MoreMenuItem item6 = new MoreMenuItem("SharePicture", "SharePicture", MoreMenuType.ON_MINI_PROGRAM);
items.add(item6);
MoreMenuItem item7 = new MoreMenuItem("Restart", "Restart", MoreMenuType.COMMON);
items.add(item7);
MoreMenuItem item8 = new MoreMenuItem("Desktop", "Desktop", MoreMenuType.COMMON);
items.add(item8);
return items;
}
@Override
public void onRegisteredMoreMenuItemClicked(@NotNull String appId, @NotNull String path, @NotNull String menuItemId, @Nullable String appInfo, @Nullable Bitmap bitmap, @NotNull IAppletCallback callback) {
Toast.makeText(mContext, "小程序" + appId + "的" + path + "页面的菜单" + menuItemId + "被点击了,appInfo : " + appInfo + " bitmap : " + bitmap, Toast.LENGTH_SHORT).show();
callback.onSuccess(null);
}
}
MoreMenuItem
为菜单条目数据类,如下:
/**
* 更多菜单条目
*
* @param id 菜单条目ID
* @param title 菜单菜单条目标题
* @param image 菜单条目图标地址
* @param icon 菜单条目图标对应的资源ID
* @param type 菜单条目类型
* @param isEnable 菜单条目是否可用
*/
data class MoreMenuItem(val id: String,
val title: String,
val image: String,
@DrawableRes val icon: Int,
val type: MoreMenuType = MoreMenuType.COMMON,
val isEnable: Boolean = true) {
/**
* 构造方法
* @param id 菜单条目ID
* @param title 菜单菜单条目标题
* @param type 菜单条目类型[MoreMenuType.COMMON]或[MoreMenuType.ON_MINI_PROGRAM]
*/
constructor(id: String, title: String, type: MoreMenuType) : this(id, title, "", -1, type, true)
}
MoreMenuType
是一个枚举类,如下:
/**
* 更多菜单类型
* [COMMON]为普通菜单类型,不需要和小程序有交互
* [ON_MINI_PROGRAM]为需要和小程序有交互的菜单类型,例如分享小程序按钮,点击按钮分享小程序时,可能需要获取到小程序的一些数据
*/
enum class MoreMenuType {
COMMON, ON_MINI_PROGRAM
}
# 2.11 如何获取小程序当前页面截图?
答:通过调用IAppletApiManager
接口的captureAppletPicture
方法获取,如下:
FinAppClient.appletApiManager.captureAppletPicture(
"appId",
snapShotWholePage,
object : FinCallback<Bitmap?> {
override fun onSuccess(result: Bitmap?) {
Log.d(TAG, "获取小程序页面截图成功 :$result")
}
override fun onError(code: Int, error: String?) {
Log.e(TAG, "获取小程序页面截图失败 :$code, $error")
}
override fun onProgress(status: Int, info: String?) {
}
})
snapShotWholePage为是否截取完整小程序页面(包括可视范围之外),默认为true,即截图范围会包括可视范围的可滚动区域,否则相反。
# 2.12 如何屏蔽更多菜单中的“转发”按钮?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否隐藏"更多"菜单中的"转发"按钮
uiConfig.setHideForwardMenu(true);
# 2.13 如何屏蔽更多菜单中的“设置”按钮?
答:”设置“菜单中展示每个小程序自身Scope权限的情况,若想要屏蔽”设置“入口,在初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否隐藏"更多"菜单中的"设置"按钮
uiConfig.setHideSettingMenu(true);
# 2.14 如何屏蔽更多菜单中的“返回首页”按钮?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否隐藏"更多"菜单中的"返回首页"菜单入口
uiConfig.setHideBackHome(true);
注:
更多菜单中的“返回首页”按钮从2.36.1版本之后已被废弃,该配置项变更为控制导航栏上的“返回首页”按钮。
# 2.15 如何屏蔽更多菜单中的“反馈与投诉”入口?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否隐藏更多菜单中的"反馈与投诉"
uiConfig.setHideFeedbackAndComplaints(true);
# 2.16 如何显示更多菜单中的“分享”按钮?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否显示更多菜单中的"分享"
// 默认值为 true 隐藏
uiConfig.setHideShareAppletMenu(false);
# 2.17 如何隐藏导航栏中的关闭按钮?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否隐藏右上角关闭按钮
uiConfig.setHideNavigationBarCloseButton(true);
# 2.18 当导航栏为默认样式时,怎样实现在首页也显示返回按钮?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 当导航栏为默认导航栏时,是否始终显示返回按钮
uiConfig.setAlwaysShowBackInDefaultNavigationBar(false);
# 2.19 怎样配置灰度发布规则?
答:和通过抽象业务回调接口IAppletHandler
实现小程序分享一样, 灰度发布配置参数的注入也是通过IAppletHandler
来实现的,IAppletHandler
会把获取灰度发布配置参数的接口方法getGrayAppletVersionConfigs
回调给宿主应用,由宿主应用实现具体的业务逻辑。
getGrayAppletVersionConfigs
如下:
/**
* 获取灰度发布配置参数
*
* @param appId 小程序ID
* @return 灰度发布配置参数
*/
fun getGrayAppletVersionConfigs(appId: String): List<GrayAppletVersionConfig>?
同样,IAppletHandler
实例需要通过调用IAppletApiManager
的setAppletHandler(appletHandler: IAppletHandler)
方法传入。
# 2.20 怎样实现自定义导航栏?
答:目前提供了三种导航栏样式,分别是:
- 默认样式(default);
- 只保留“更多关闭”按钮的自定义样式(custom);
- 不保留“更多关闭”按钮,原生导航栏全部隐藏的自定义样式(hide)。
默认情况下,小程序中page
的navigationStyle
和window
的navigationStyle
都为default,如果开发者需要自定义导航栏样式,可以通过配置page
或者window
的navigationStyle
来实现。
# 2.21 是否支持禁止SDK主动发起SDK权限申请?
答:支持。
如果宿主应用希望所有SDK权限的申请都交由自己管理,不想SDK在用到权限时主动发起申请,那么可以通过在初始化SDK时配置参数来实现,如下:
FinAppConfig finAppConfig = new FinAppConfig.Builder()
.setDisableRequestPermissions(true) // 禁止SDK发起运行时权限申请
.build();
# 2.22 是否支持配置小程序申请Scope权限时自动授予?
答:支持。
Scope权限可在小程序的”更多“-”设置“中查看、开启、关闭,默认情况下每个小程序第一次申请Scope权限时都会弹出对话框向用户申请,若SDK希望自动授予Scope权限,不向用户弹框的话,可以在初始化SDK时配置参数来实现,如下:
FinAppConfig finAppConfig = new FinAppConfig.Builder()
.setAppletAutoAuthorize(true) // 自动授予Scope权限
.build();
# 2.23 怎样调整导航栏标题文字样式?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 导航栏标题文字样式
uiConfig.setNavigationBarTitleTextAppearance(R.style.TextAppearance_AppCompat);
# 2.24 怎样让导航栏标题居中显示?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 导航栏标题相对父控件的Gravity
uiConfig.setNavigationBarTitleTextLayoutGravity(Gravity.CENTER);
# 2.25 怎样去除导航栏返回按钮按下时的背景动画?
答:初始化SDK时,通过UI配置项进行配置,如下:
// UI配置
FinAppConfig.UIConfig uiConfig = new FinAppConfig.UIConfig();
// 是否清除导航栏导航按钮的背景
uiConfig.setClearNavigationBarNavButtonBackground(true);
# 2.26 小程序是否支持横竖屏切换?
答:支持。
小程序支持横屏、竖屏、横竖屏自由切换。小程序的屏幕旋转设置可以在小程序工程的app.json
文件的window
中全局配置,也可以在每个页面的.json
文件中单独配置。
- 在
app.json
中全局配置:
{
"window": {
"pageOrientation": "auto" // auto:横竖屏自由切换,portrait:竖屏,landscape:横屏
}
}
- 在页面的
.json
文件中单独配置:
{
"pageOrientation": "auto" // auto:横竖屏自由切换,portrait:竖屏,landscape:横屏
}
- 页面配置在当前页面会覆盖
app.json
的window
中相同的配置。
# 2.27 怎样禁止使用TBS SDK?
答:初始化SDK时通过配置参数来实现,如下:
FinAppConfig finAppConfig = new FinAppConfig.Builder()
.setDisableTbs(true) // 设置是否禁止启用Tbs SDK
.build();
# 2.28 怎样设置定时批量更新小程序的数量?
答:初始化SDK时通过配置参数来实现,如下:
FinAppConfig finAppConfig = new FinAppConfig.Builder()
.setAppletIntervalUpdateLimit(3) // 设置定时批量更新小程序的数量
.build();
# 2.29 怎样设置可同时打开的小程序的个数?
答:初始化SDK时通过配置参数来实现,如下:
FinAppConfig finAppConfig = new FinAppConfig.Builder()
.setMaxRunningApplet(3) // 可同时可打开的小程序的个数
.build();
# 2.30 小程序支持控制右上角更多
按钮的显示和隐藏吗?
答:支持。
配置navigationBarHideMoreButton
属性即可实现,navigationBarHideMoreButton
可以在小程序工程的app.json
文件的window
中全局配置,也可以在每个页面的.json
文件中单独配置。
- 在
app.json
中全局配置:
{
"window": {
"navigationBarHideMoreButton": true // true:隐藏右上角更多按钮,false:显示右上角更多按钮,默认为false。
}
}
- 在页面的
.json
文件中单独配置:
{
"navigationBarHideMoreButton": true // true:隐藏右上角更多按钮,false:显示右上角更多按钮,默认为false。
}
# 2.31 小程序可以控制右上角关闭按钮的显示和隐藏吗?
可以。配置navigationBarHideCloseButton
属性即可实现,navigationBarHideCloseButton
可以在小程序工程的app.json文件的window中全局配置,也可以在每个页面的.json
文件中单独配置。
- 在
app.json
中全局配置:
{
"window": {
"navigationBarHideCloseButton": true // true:隐藏右上角关闭按钮,false:显示右上角关闭按钮,默认为false。
}
}
- 在页面的
.json
文件中单独配置:
{
"navigationBarHideCloseButton": true // true:隐藏右上角关闭按钮,false:显示右上角关闭按钮,默认为false。
}
# 2.32 是否支持把SDK中“小程序”文案替换为其它名称?
答:支持。
通过SDK初始化配置参数来实现,如下:
FinAppConfig finAppConfig = new FinAppConfig.Builder()
.setAppletText("X应用") // 把SDK中的“小程序”文案替换为“X应用”
.build();
# 2.33 数据上报时,是否会对上报的数据进行压缩?
答:数据上报时,SDK默认不对上报的数据进行压缩,如果要开启压缩,可以通过SDK初始化配置参数来实现,如下:
FinAppConfig finAppConfig = new FinAppConfig.Builder()
.setEnableApmDataCompression(true) // 设置数据上报时,对上报的数据进行压缩
.build();
# 2.34 是否支持禁用获取监管信息的小程序API?
答:支持。
SDK默认允许小程序调用获取监管信息的小程序API(getSuperviseInfo),如果要禁用,则可以通过SDK初始化配置参数来实现,如下:
FinAppConfig finAppConfig = new FinAppConfig.Builder()
.setDisableGetSuperviseInfo(true) // 设置是否禁止小程序调用获取监管信息的小程序API
.build();
禁止后,小程序调用getSuperviseInfo
时将会收到getSuperviseInfo:fail disabled
回调
# 2.35 是否支持打开体验版小程序?
答:支持。
平台支持为小程序配置体验版本和体验成员,拥有体验权限的成员能够打开体验版小程序。
使用体验版小程序的步骤:
- 在平台中上传小程序,并为小程序配置体验版
- 在初始化SDK的时候传入用户ID:
val config = FinAppConfig.Builder()
.setUserId("用户ID")
.build()
只有当传入的用户ID在小程序配置的成员列表中时,才能打开体验版小程序,否则在打开小程序的时候,前置页面中会提示“无体验权限”。
- 通过调用SDK提供的接口打开小程序,接口如下:
/**
* 启动小程序
*
* @param context 上下文
* @param startAppletDecryptRequest 请求体
*/
fun startApplet(context: Context, startAppletDecryptRequest: StartAppletDecryptRequest)`
StartAppletDecryptRequest
的结构如下:
/**
* 启动小程序请求实体类
*
* @param info 小程序加密信息
*/
data class StartAppletDecryptRequest(val info: String)
StartAppletDecryptRequest
的info
字段表示小程序加密信息,和小程序体验版二维码中的info
字段对应。因此打开体验版小程序时,StartAppletDecryptRequest
的info
字段传小程序体验版二维码中的info字段对应的value即可。
以下面这段体验版小程序二维码内容为例:
https://finchat-mop.finogeeks.club/mop/scattered-page/#/sdktip?type=scanOpen&info=SFODj9IW1ENO8OA0El8P79aMuxB1DJvfKenZd7hrnemVCNcJ+Uj9PzkRkf/Pu5nMz0cGjj0Ne4fcchBRCmJO+As0XFqMrOclsqrXaogsaUPq2jJKCCao03vI8rkHilrWxSDdzopz1ifJCgFC9d6v29m9jU29wTxlHsQUtKsk/wz0BROa+aDGWh0rKvUEPgo8mB+40/zZFNsRZ0PjsQsi7GdLg8p4igKyRYtRgOxUq37wgDU4Ymn/yeXvOv7KrzUT&codeType=trial
则info
字段值应为:
SFODj9IW1ENO8OA0El8P79aMuxB1DJvfKenZd7hrnemVCNcJ+Uj9PzkRkf/Pu5nMz0cGjj0Ne4fcchBRCmJO+As0XFqMrOclsqrXaogsaUPq2jJKCCao03vI8rkHilrWxSDdzopz1ifJCgFC9d6v29m9jU29wTxlHsQUtKsk/wz0BROa+aDGWh0rKvUEPgo8mB+40/zZFNsRZ0PjsQsi7GdLg8p4igKyRYtRgOxUq37wgDU4Ymn/yeXvOv7KrzUT
# 2.36 怎么在小程序中实现第三方登录?
# 2.36.1集成 “人民网+”小程序 SDK
开发者首先需要集成 “人民网+”小程序 SDK,集成指南请参照 “人民网+”小程序 小程序开放平台 Android 集成文档,开放平台已有详尽的 Android 集成文档,此处不再赘述。
# 2.36.2 自定义小程序接口以实现授权登录
为了让小程序能过获取到小程序以外的 APP 数据,需要注册小程序自定义接口,自定义接口具体说明请参照 “人民网+”小程序 小程序开放平台-自定义小程序接口。
注意
本例中的参数在实际开发中由开发者自行制定,本例仅为示范作用。
- 自定义授权登录 login 接口
因本示例在授权登录时需要展示授权 Dialog,即需要获取 Activity 实例,因此我们需要该 Api 注册在小程序进程,可以方便的获取到展示小程序的 Activity 实例。
public class LoginApi extends AbsApi {
// 定义代码省略
private void showAuthDialog(ICallback iCallback) {
new AlertDialog.Builder(activity)
.setTitle("授权登录")
.setMessage("是否授权该小程序获取用户信息?")
.setCancelable(false)
.setPositiveButton("确定", (dialog, which) -> authLoginOnMainProcess(iCallback))
.setNegativeButton("取消", (dialog, which) -> iCallback.onFail())
.show();
}
/**
* 由于用户信息一般只会存储在主进程中,在小程序进程中直接调用取不到数据
* 因此要使用 callInMainProcess 方法跨进程调用,在主进程中获取到信息后,再回传给小程序进程
*/
private void authLoginOnMainProcess(ICallback iCallback) {
// 跨进程调用代码省略
}
}
跨进程调用 api 的相关说明可以查看文档:小程序进程调用主进程
在小程序进程中注册自定义 api
if (FinAppClient.INSTANCE.isFinAppProcess(this)) {
// 小程序进程
initFinClipOnAppletProcess();
} else {
// 主进程初始化代码省略
}
/**
* 将小程序注册到小程序进程中
*/
private void initFinClipOnAppletProcess() {
FinAppProcessClient.INSTANCE.setCallback(new FinAppProcessClient.Callback() {
@Override
public List<IApi> getRegisterExtensionApis(@NotNull Activity activity) {
List<IApi> extensionApis = new ArrayList<>();
extensionApis.add(new LoginApi(activity));
return extensionApis;
}
@Override
public List<IApi> getRegisterExtensionWebApis(@NotNull Activity activity) {
return null;
}
});
}
至此,小程序通过自定义 Api 从 APP 获取用户token的整个流程就已经完成了。
注意
如果产品需求不需要展示用户授权提示 Dialog,建议在主进程注册自定义 Api,从而省掉上述跨进程调用的过程。
# 2.36.3 自定义小程序接口以实现获取用户信息
本例中的参数在实际开发中由开发者自行制定,本例仅为示范作用。
自定义获取用户信息 getUserProfile 接口
public class ProfileApi extends AbsApi {
// 定义代码省略
/**
* 此示例中 ProfileApi 直接注册在了主进程的扩展 api 中
* 因此该 api 是在主进程中执行,可以直接获取数据
*/
private void getUserProfile(JSONObject jsonObject, ICallback iCallback) {
// 获取用户信息过程省略
}
}
在主进程中注册 api
FinCallback<Object> initCallback = new FinCallback<Object>() {
@Override
public void onSuccess(Object result) {
// 注册扩展Api,此处注册的Api将会在主进程中执行
FinAppClient.INSTANCE
.getExtensionApiManager()
.registerApi(new ProfileApi());
}
@Override
public void onError(int code, String error) {
}
@Override
public void onProgress(int status, String error) {
}
};
FinAppClient.INSTANCE.init(this, finAppConfig, initCallback);
至此,小程序通过自定义 Api 从 APP 获取用户信息的整个流程就已经完成了。
# 2.36.4自定义小程序接口以检查用户token是否已失效
本例中的参数在实际开发中由开发者自行制定,本例仅为示范作用。
自定义检查用户token的checkSession 接口
public class AppletSessionApi extends AbsApi {
// 定义代码省略
private void checkSession(JSONObject jsonObject, ICallback iCallback) {
// 检查过程代码省略
}
}
在主进程中注册 api
FinCallback<Object> initCallback = new FinCallback<Object>() {
@Override
public void onSuccess(Object result) {
// 注册扩展Api,此处注册的Api将会在主进程中执行
FinAppClient.INSTANCE
.getExtensionApiManager()
.registerApi(new ProfileApi());
FinAppClient.INSTANCE
.getExtensionApiManager()
.registerApi(new AppletSessionApi());
}
@Override
public void onError(int code, String error) {
}
@Override
public void onProgress(int status, String error) {
}
};
FinAppClient.INSTANCE.init(this, finAppConfig, initCallback);
至此,小程序通过自定义 Api 从 APP 检查用户token的整个流程就已经完成了。
# 2.37 是否支持离线小程序,使用本地小程序提高首次加载速度
从2.35.1版本开始,sdk支持打开小程序时指定本地基础库和小程序压缩包的路径,此时会使用本地代码包直接打开小程序,提高加载速度。 后续小程序依然能正常从后端获取更新
接口定义
/**
* 启动小程序
*
* @param context 上下文
* @param apiServer 小程序所在应用市场的服务器地址
* @param appId 小程序id
* @param startParams 启动小程序时携带的参数
* @param offlineLibraryPath 离线基础库路径
* @param offlineAppletPath 本地小程序包的路径
*/
fun startApplet(context: Context, apiServer: String, appId: String, startParams: FinAppInfo.StartParams? = null, offlineLibraryPath: String, offlineAppletPath: String)
调用示例
FinAppClient.appletApiManager.startApplet(this, "https://api.finclip.com",
"617bb42f530fb30001509b27", null,
"$filesDir/framework-2.11.4-alpha20211101v01.zip", "$filesDir/分包跳转测试-1.1.1.zip")
# 2.38 小程序支持自定义菜单吗?
答:支持。
通过实现代理方法,重写onNavigationBarMoreButtonClicked()
方法,显示自定义菜单即可。
- 通过调用IAppletProcessApiManager的setAppletProcessHandler(appletProcessHandler: IAppletProcessHandler)方法传入IAppletProcessHandler实例,在
onNavigationBarMoreButtonClicked()
方法内实现显示自定义菜单,如下:
FinAppProcessClient.INSTANCE.getAppletProcessApiManager().setAppletProcessHandler(new IAppletProcessHandler() {
@Override
public boolean onNavigationBarMoreButtonClicked(@NonNull Context context, @NonNull String appId) {
// 显示自定义菜单
…………………………………………
…………………………………………
return true; // 返回true表示自行处理"更多"按钮点击事件,屏蔽默认菜单显示逻辑。
}
});
注意
FinAppProcessClient类需要在小程序进程使用。
请使用
FinAppClient.INSTANCE.isFinAppProcess()
方法判断是否处于小程序进程IAppletProcessHandler接口方法在小程序进程执行。
- 使用
MoreMenuHelper
类提供的方法实现SDK默认菜单项的功能。 方法定义如下:
/**
* 触发转发动作,与SDK默认菜单“转发”行为一致,需要在代理方法shareAppMessage()内处理转发逻辑
*/
fun invokeForwardMenuAction(context: Context)
/**
* 反馈与投诉
*/
fun goToFeedbackPage(context: Context)
/**
* 打开关于页面
*/
fun goToAboutPage(context: Context)
/**
* 打开设置页面
*/
fun goToSettingPage(context: Context)
/**
* 打开或关闭小程序调试(vConsole)
*
* @param enableAppletDebug true 打开, false 关闭
*/
fun setEnableAppletDebug(context: Context, enableAppletDebug: Boolean)
/**
* 是否打开或关闭了小程序调试(vConsole)
*/
fun isEnableAppletDebug(context: Context): Boolean
/**
* 获取[MoreMenuType.ON_MINI_PROGRAM]类型的菜单数据
*
* @param callback 参数字段说明请参考代理方法onRegisteredMoreMenuItemClicked()
*/
fun getMiniProgramTypeMenuData(
context: Context,
menuId: String?,
callback: (appId: String, path: String, menuItemId: String, appInfo: String?, bitmap: Bitmap?) -> Unit
)
/**
* 检测小程序是否实现自定义菜单功能
* 其中onShareAppMessage事件受小程序是否调用了showShareMenu/hideShareMenu API影响
* 若小程序调用了showShareMenu后调用该方法检测,则无论小程序是否实现onShareAppMessage事件,该事件对应的value为true
* 若小程序调用了hideShareMenu后调用该方法检测,则无论小程序是否实现onShareAppMessage事件,该事件对应的value为false
*
* @param callback result JSONArray元素为{"eventName":"小程序事件名","menuId":"菜单id","value":"事件是否实现"}
*/
fun checkMenus(
activity: FinAppHomeActivity,
pageWebViewId: Int?,
menuIds: List<String>,
callback: (result: JSONArray) -> Unit
)
注意
MoreMenuHelper类需要在小程序进程使用。
示例:
public class App extends Application {
private final Handler handler = new Handler();
private static final String CUSTOM_MENU_ID = "customMenu";
@Override
public void onCreate() {
super.onCreate();
if (!FinAppClient.INSTANCE.isFinAppProcess(this)) {
initFinClipOnMainProcess(); // 主进程
} else {
initFinClipOnAppletProcess(); // 小程序进程
}
}
private void initFinClipOnMainProcess() {
// 初始化配置...
FinCallback<Object> callback = new FinCallback<Object>() {
@Override
public void onSuccess(Object result) { // SDK初始化成功
setAppletHandler();
}
@Override
public void onError(int code, String error) { // SDK初始化失败
Toast.makeText(App2.this, "SDK初始化失败", Toast.LENGTH_SHORT).show();
}
@Override
public void onProgress(int status, String error) {
}
};
// 初始化FinClipSDK
FinAppClient.INSTANCE.init(this, config, callback);
}
private void setAppletHandler() {
FinAppClient.INSTANCE.getAppletApiManager().setAppletHandler(new IAppletHandler() {
/**
* 转发小程序
*
* @param appInfo 小程序信息,是一串json,包含了小程序id、小程序名称、小程序图标、用户id、转发的数据内容等信息。
* @param bitmap 小程序封面图片。如果[appInfo].params.imageUrl字段为http、https的链接地址,那么小程序封面图片
* 就取[appInfo].params.imageUrl对应的图片,否则小程序的封面图片取[bitmap]。
* @param callback 转发小程序结果回调。
*
* 方法说明请参考:https://www.finclip.com/mop/document/runtime-sdk/android/android-issue.html#_2-9-%E6%80%8E%E4%B9%88%E5%88%86%E4%BA%AB%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%88%B0%E5%BE%AE%E4%BF%A1%E7%AD%89%E6%94%AF%E6%8C%81%E5%B0%8F%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%B9%B3%E5%8F%B0
*/
@Override
public void shareAppMessage(@NotNull String appInfo, @Nullable Bitmap bitmap, @NotNull IAppletCallback callback) {
handler.post(new Runnable() {
@Override
public void run() {
// 转发/分享...
callback.onSuccess(null);
}
});
}
});
}
/**
* 设置在小程序进程执行的代理方法
*/
private void initFinClipOnAppletProcess() {
FinAppProcessClient.INSTANCE.getAppletProcessApiManager().setAppletProcessHandler(new IAppletProcessHandler() {
/**
* 小程序导航栏中的"更多"按钮被点击
*
* @param appId 小程序ID
* @return 返回true表示自行处理按钮点击事件,不需要执行默认操作(弹出菜单)。返回false表示需要执行默认操作
*/
@Override
public boolean onNavigationBarMoreButtonClicked(@NotNull Context context, @NotNull String appId) {
// 注意:这里是小程序进程
// 分享配置请参考:
// https://www.finclip.com/mop/document/develop/api/custom-menu.html#_2-2-%E5%88%86%E4%BA%AB%E9%85%8D%E7%BD%AE
ArrayList<String> menuIds = new ArrayList<>();
menuIds.add("WXShareAPPFriends"); // 菜单配置信息在小程序onShareAppMessage方法提供
menuIds.add("WXShareAPPMoments"); // 菜单配置信息在小程序onShareAppMessage方法提供
menuIds.add(CUSTOM_MENU_ID); // 菜单配置信息在小程序onCustomMenuButtonHandler方法提供
MoreMenuHelper.checkMenus(context, menuIds, new Function1<JSONArray, Unit>() {
@Override
public Unit invoke(JSONArray result) {
// 注意:这里是小程序进程
boolean forwardMenuEnable = false; // 转发菜单是否可用
boolean customMenuEnable = false; // custom菜单是否可用
// 遍历获取按钮可用状态
for (int i = 0; i < result.length(); i++) {
JSONObject jsonObject = (JSONObject) result.opt(i);
if (jsonObject != null) {
String eventName = jsonObject.optString("eventName");
if (eventName.equals("onShareAppMessage")) { // 菜单Id为WXShareAPPFriends或WXShareAPPMoments
boolean value = jsonObject.optBoolean("value");
if (value) {
forwardMenuEnable = true;
}
}
if (eventName.equals("onCustomMenuButtonHandler")) { // 菜单Id为customMenu
boolean value = jsonObject.optBoolean("value");
if (value) {
customMenuEnable = true;
}
}
}
}
showMenuDialog(context, forwardMenuEnable, customMenuEnable);
return null;
}
});
return true;
}
});
}
private void showMenuDialog(Context context, boolean forwardMenuEnable, boolean customMenuEnable) {
String enableAppletDebug;
if (MoreMenuHelper.isEnableAppletDebug(context)) {
enableAppletDebug = "关闭调试";
} else {
enableAppletDebug = "打开调试";
}
new MenuDialog(context)
// 根据方法参数设置菜单按钮状态,此处省略...
.setListener(new BottomSheetListener() {
/**
* 菜单按钮点击事件
*/
@Override
public void onSheetItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.about) {
// 关于页面
MoreMenuHelper.goToAboutPage(context);
} else if (itemId == R.id.setting) {
// 设置页面
MoreMenuHelper.goToSettingPage(context);
} else if (itemId == R.id.feedback) {
// 反馈与投诉
MoreMenuHelper.goToFeedbackPage(context);
} else if (itemId == R.id.forward) {
// 触发转发动作,与SDK默认菜单“转发”行为一致,需要在代理方法shareAppMessage()内处理转发逻辑
// https://www.finclip.com/mop/document/runtime-sdk/android/android-issue.html#_2-9-%E6%80%8E%E4%B9%88%E5%88%86%E4%BA%AB%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%88%B0%E5%BE%AE%E4%BF%A1%E7%AD%89%E6%94%AF%E6%8C%81%E5%B0%8F%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%B9%B3%E5%8F%B0
MoreMenuHelper.invokeForwardMenuAction(context);
} else if (itemId == R.id.customMenu) { // 这个菜单需要小程序信息
// 获取[MoreMenuType.ON_MINI_PROGRAM]类型的菜单数据
// 参数字段说明请参考代理方法onRegisteredMoreMenuItemClicked():
// https://www.finclip.com/mop/document/runtime-sdk/android/android-issue.html#_2-10-%E5%A6%82%E4%BD%95%E5%BE%80-%E6%9B%B4%E5%A4%9A-%E8%8F%9C%E5%8D%95%E4%B8%AD%E6%B3%A8%E5%85%A5%E8%87%AA%E5%B7%B1%E7%9A%84%E8%8F%9C%E5%8D%95%E9%A1%B9
MoreMenuHelper.getMiniProgramTypeMenuData(context, CUSTOM_MENU_ID, new Function5<String, String, String, String, Bitmap, Unit>() {
@Override
public Unit invoke(String appId, String path, String menuItemId, String appInfo, Bitmap bitmap) {
// 注意:这里是小程序进程
// customMenu菜单业务逻辑...
return null;
}
});
} else {
// 打开或关闭小程序调试(vConsole)
MoreMenuHelper.setEnableAppletDebug(context, !MoreMenuHelper.isEnableAppletDebug(context));
}
}
})
.show();
}
}
# 2.39 小程序是否可以单任务运行?
答:可以。
启动小程序时,SDK默认以多任务方式运行小程序,最直观的表现是在系统近期任务列表中,宿主APP和小程序是分不同的任务展示的。SDK允许设置小程序以单任务方式运行小程序,即在系统近期任务列表中,宿主APP和小程序在同一个任务中展示。设置方法如下:
调用启动小程序接口时,设置IFinAppletRequest
对象的isSingleTask
字段值为true
IFinAppletRequest
:
/**
* 标识是否通过单任务栈方式打开小程序
*/
internal var isSingleTask: Boolean = false
/**
* 设置是否通过单任务栈方式打开小程序
*/
fun setSingleTask(isSingleTask: Boolean): IFinAppletRequest {
this.isSingleTask = isSingleTask
return this
}
示例:
FinAppClient.INSTANCE.getAppletApiManager().startApplet(this, IFinAppletRequest.Companion.fromAppId("小程序ID").setSingleTask(true), null);
# 2.40 如何使用J2V8加载小程序?
SDK支持使用J2V8加载小程序,启用J2V8的方法如下:
在Android工程最外层的build.gradle文件中添加jitpack仓库:
allprojects { repositories { ... 其它仓库 ... maven { url "https://jitpack.io" } ... 其它仓库 ... } }
在Android App module的build.gradle文件中添加如下依赖:
// j2v8 implementation 'com.eclipsesource.j2v8:j2v8:6.2.1@aar' // j2v8-debugger // 如果要通过Chrome DevTools调试J2V8,则须依赖j2v8-debugger,反之可不依赖j2v8-debugger implementation('com.github.AlexTrotsenko:j2v8-debugger:0.2.3') { exclude group: 'com.eclipsesource.j2v8' exclude group: 'com.android.support' }
如果依赖了j2v8-debugger,编译APP工程报如下错误:
Manifest merger failed : uses-sdk:minSdkVersion 19 cannot be smaller than version 23 declared in library [com.github.AlexTrotsenko:j2v8-debugger:0.2.3] xxx/j2v8-debugger-0.2.3/AndroidManifest.xml as the library might be using APIs not available in 19 Suggestion: use a compatible library with a minSdk of at most 19, or increase this project's minSdk version to at least 23, or use tools:overrideLibrary="com.alexii.j2v8debugger" to force usage (may lead to runtime failures)
则需要在App module的AndroidManifest.xml文件中添加如下代码:
<uses-sdk tools:overrideLibrary="com.alexii.j2v8debugger" />
在App module中添加J2V8混淆规则,如下:
# J2V8 -keep class com.eclipsesource.v8.** {*;}
# 2.41 “人民网+”小程序 SDK支持设置语言吗?如何设置?
“人民网+”小程序 Android SDK 自2.40.0-alpha20230106v02 开始支持设置SDK的语言类型,这里的语言类型会影响SDK中(比如更多面板、关于、设置、投诉反馈等)公共UI中文字的语言。目前仅支持简体中文 和 英文,设置其他语言时,会显示默认值 简体中文。
如何设置? FinAppConfig中有一个locale的配置项,初始化的时候设置该值即可。
示例代码:
val config = FinAppConfig.Builder()
.setLocale(Locale.SIMPLIFIED_CHINESE)
.build()
# 3. 调试方面
# 3.1 开发小程序的时候,用什么工具调试小程序?
答:Android小程序SDK使用了腾讯TBS浏览服务(X5内核),调试工具为TBS Studio。TBS Studio能够像在Chrome浏览器中调试网页那样对小程序进行调试。
注意
从 2.33.3
版本开始,SDK已使用系统webview替换X5内核。
# 3.2 为什么TBS Studio无法开启调试,一直提示当前目标App不能进行TBS调试,请进行如下检查和操作 请确保当前目标App的Webview基于TBS开发 请确保前序步骤执行成功?
答:出现这种情况,一般是因为当前设备为64位处理器的设备,且没有让应用以32位模式运行。43903版本之前的TBS SDK不提供64位的so动态库,如果在64位处理器设备上仍以64位模式运行,那么TBS SDK在初始化的时候便会失败,X5内核将不能成功启用。
要让64位处理器的设备能够正常启用X5内核,需要在当前Android工程的build.gradle
文件的defaultConfig
中进行如下设置,让应用以32位模式运行。
ndk {
// 设置支持的SO库架构
abiFilters "armeabi", 'armeabi-v7a'
}
如果配置后编译报错,则可以通过在工程的gradle.properties
文件中增加如下配置来解决。
Android.useDeprecatedNdk=true;
# 3.3 为什么在一些低版本系统中,小程序加载网页会一直白屏,而在高版本系统中则不会?
答:出现这种情况,一般是因为低版本系统中的浏览器版本也比较低,浏览器无法正常识别新语法特性导致的。例如:有些浏览器版本的发布早于ES6的定稿和发布,如果在编码的时候使用了ES6的新特性,而浏览器并没有更新版本,那么当在浏览器中打开网页时,浏览器就会无法识别ES6代码,从而发生错误。
对于这类问题,一般建议:
- 在编码的时候,尽可能使用兼容性好的语法;
- 如果不可避免地需要使用一些新语法特性,可以尝试引入一些语法转换工具,如babel-polyfill等,将新的语法自动转换为低版本的语法,这样你就可以在使用新语法特性的时候不用考虑环境兼容的问题。
# 3.4 如何开启vConsole调试小程序?
如果需要使用vConsole调试小程序,目前有多种方式开启Debug模式。
- SDK初始化配置中的
setEnableAppletDebug
为true,可使所有小程序显示vconsole。请在应用上线时保持该配置为false状态。(设置为true时,无法在小程序内通过api方式关闭vConsole)。 - SDK初始化配置中的
setEnableAppletDebug
为false时,每个小程序可独立开启Debug模式。非线上版本(比如体验版、审核版、开发版、预览版)可在更多菜单中通过【打开调试】开启Debug模式。 - SDK初始化配置中的
setEnableAppletDebug
为false时,通过小程序调用api(ft.setEnableDebug
)来独立开启Debug模式。
如果需要调试小程序,目前有多种方式开启vconsole,从而可以看到小程序中的日志。 开关vconsole,要从SDK和小程序两个方面来说。
SDK方面 SDK里有两个地方影响vconsole的开启和关闭:
- 初始化配置项FinAppConfig中的appletDebugMode参数;
- 更多面板里的 【打开调试】和【关闭调试】按钮。
appletDebugMode是个枚举类型,目前一共有四个值:
- appletDebugModeUndefined:默认值。
- appletDebugModeEnable:强制所有小程序(所有版本)均开启vconsole,并且更多面板里不显示【打开调试】和【关闭调试】按钮。
- appletDebugModeDisable:类似微信的效果,小程序正式版更多面板里不显示【打开调试】和【关闭调试】按钮。但是非线上版 更多面板里会显示【打开调试】和【关闭调试】按钮。
- appletDebugModeForbidden:强制所有小程序(所有版本)均不开启vconsole,并且更多面板里不显示【打开调试】和【关闭调试】按钮。
小程序方面
小程序里可以调用小程序api(ft.setEnableDebug
)来开启vconsole。
但是,如果appletDebugMode
设置为appletDebugModeEnable
,则ft.setEnableDebug
接口不生效。
如果appletDebugMode
设置为appletDebugModeForbidden
,则ft.setEnableDebug
接口也不生效。
最后,建议在app开发阶段,设置appletDebugMode
为appletDebugModeEnable
;然后在App提交审核时,修改为appletDebugModeDisable
。
因为设置为appletDebugModeDisable
,正式版小程序 不会显示 【打开调试】和【关闭调试】按钮,但是依然可以通过api(ft.setEnableDebug
)来开启vconsole。
而非正式版小程序,可以通过【打开调试】和ft.setEnableDebug
来开启vconsole。