# 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 怎么启动小程序?

  1. 如果启动小程序时不携带启动参数,则通过调用IAppletApiManager接口的startApplet(context: Context, appId: String)方法启动,如下:
FinAppClient.INSTANCE.getAppletApiManager().startApplet(context, "appId");
  1. 如果启动小程序时携带启动参数,则通过调用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内部的标准接口一样,可以供小程序调用。

具体实现步骤如下:

  1. 实现自定义小程序接口。
    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();
            }
        }
    }
}
  1. 注册自定义小程序接口。
    通过调用IExtensionApiManager接口的registerApi方法实现注册。如下:
FinAppClient.INSTANCE.getExtensionApiManager().registerApi(new ApiOpenPage(context));

如果需要一次注册多个接口,则可以调用registerApis,传入的参数为接口集合。

  1. 在小程序工程中增加扩展接口配置。SDK注册自定义小程序接口后,还需要在小程序工程的根目录创建FinClipConf.js文件,在FinClipConf.js中配置对应的自定义接口。配置示例如下:
module.exports = {
  	extApi:[
    	{
      		name: 'openPage', // 扩展接口名
      		params: { // 扩展接口参数,可以只列必须的参数
        		url: '',
        		title: '',
        		description: ''
      		}
    	}
  	]
}
  1. 在小程序中调用已注册接口中的各个API。

# 2.4 小程序加载网页时,网页可以调用原生的代码吗?

答:可以在小程序的网页中调用原生代码。JSSDK提供了一系列接口,通过这些接口可以实现在网页里面调用原生代码。

# 2.5 JSSDK接口是否支持扩展?即除了JSSDK内部提供的一系列标准接口之外,能否让小程序在网页中调用自己提供的接口?如果可以要怎么实现?

答:JSSDK接口支持扩展。SDK允许注册自定义的JSSDK接口,注册之后的自定义JSSDK接口和JSSDK内部提供的标准接口一样,可以供小程序在网页中调用。

具体实现步骤如下:

  1. 实现自定义JSSDK接口。
    这一步和实现自定义小程序接口是一样的,通过继承AbsApi,并重写apis()方法和invoke()方法实现自定义JSSDK接口。
  2. 注册自定义JSSDK接口。
    通过调用IExtensionWebApiManager接口的registerApi方法实现注册。如下:
FinAppClient.INSTANCE.getExtensionWebApiManager().registerApi(new ApiOpenPage(context));

如果需要一次注册多个接口,则可以调用registerApis,传入的参数为接口集合。

详细可以查看注册小程序 WebView 组件API

# 2.6 在自定义接口的invoke()方法中跳转到宿主App的其它页面,做完一系列操作之后,按系统返回键想返回小程序,结果却返回到了宿主App中启动小程序的页面,为什么?

  1. 原因:

    跳转到宿主App其它页面这一步,是通过宿主App中的Context实例来启动Activity的,并且没有把Activity压入新的任务栈中。

    Android小程序SDK是多进程架构的,小程序和宿主App处于不同进程中,所处的任务栈自然也是不同的。小程序跳转到宿主App的页面,新打开的页面是添加到宿主App原有的任务栈中的,当从页面返回时,执行的逻辑是在原生App中原有的任务栈中弹出页面,因此会看到原生App的页面被逐个关闭,最后返回到原生应用启动小程序的页面,并没有返回小程序。

  2. 解决方案:

    方案1(推荐):
    通过ICallbackstartActivitystartActivityForResult来跳转到宿主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_TASKFLAG_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);

目前提供了五种动画可供设置:

  1. NoneAnim:无动画;
  2. SlideFromLeftToRightAnim:滑动动画-左进右出;
  3. SlideFromRightToLeftAnim:滑动动画-右进左出;
  4. SlideFromTopToBottomAnim:滑动动画-上进下出;
  5. SlideFromBottomToTopAnim:滑动动画-下进上出。

# 2.9 怎么分享小程序到微信等支持小程序的平台?

答:要实现小程序分享功能,总体思路是先获取到分享小程序所需要的相关信息,然后把获取到的信息转换为分享接口的参数,最后再调用分享接口把小程序分享到对应平台。具体实现方案主要有两种:

  1. 实现小程序抽象业务回调接口IAppletHandlershareAppMessage方法,并将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)
    

    通过调用IAppletApiManagersetAppletHandler(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) {
           // 实现分享小程序的逻辑
           ……………………………………………………
          ……………………………………………………
       }
    });
    
  2. 通过自定义接口来实现。在自定义接口的invoke方法中接收小程序传递过来的参数,然后调用第三方分享SDK实现小程序分享。

# 2.10 如何往“更多”菜单中注入自己的菜单项?

答:和通过抽象业务回调接口IAppletHandler实现小程序分享一样, “更多”菜单中菜单项的注入也是通过IAppletHandler来实现的,IAppletHandler会把获取注入菜单项的接口方法getRegisteredMoreMenuItems和点击注入菜单项的接口方法onRegisteredMoreMenuItemClicked回调给宿主应用,由宿主应用实现具体的业务逻辑。

getRegisteredMoreMenuItemsonRegisteredMoreMenuItemClicked如下:

/**
 * 获取注册的"更多"菜单项
 *
 * @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实例需要通过调用IAppletApiManagersetAppletHandler(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实例需要通过调用IAppletApiManagersetAppletHandler(appletHandler: IAppletHandler)方法传入。

# 2.20 怎样实现自定义导航栏?

答:目前提供了三种导航栏样式,分别是:

  1. 默认样式(default);
  2. 只保留“更多关闭”按钮的自定义样式(custom);
  3. 不保留“更多关闭”按钮,原生导航栏全部隐藏的自定义样式(hide)。

默认情况下,小程序中pagenavigationStylewindownavigationStyle都为default,如果开发者需要自定义导航栏样式,可以通过配置page或者windownavigationStyle来实现。

# 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文件中单独配置。

  1. app.json中全局配置:
{
	"window": {
		"pageOrientation": "auto" // auto:横竖屏自由切换,portrait:竖屏,landscape:横屏
	}
}
  1. 在页面的.json文件中单独配置:
{
	"pageOrientation": "auto" // auto:横竖屏自由切换,portrait:竖屏,landscape:横屏
}
  1. 页面配置在当前页面会覆盖 app.jsonwindow 中相同的配置。

# 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文件中单独配置。

  1. app.json中全局配置:
{
	"window": {
		"navigationBarHideMoreButton": true // true:隐藏右上角更多按钮,false:显示右上角更多按钮,默认为false。
	}
}
  1. 在页面的.json文件中单独配置:
{
	"navigationBarHideMoreButton": true // true:隐藏右上角更多按钮,false:显示右上角更多按钮,默认为false。
}

# 2.31 小程序可以控制右上角关闭按钮的显示和隐藏吗?

可以。配置navigationBarHideCloseButton属性即可实现,navigationBarHideCloseButton可以在小程序工程的app.json文件的window中全局配置,也可以在每个页面的.json文件中单独配置。

  1. app.json中全局配置:
{
	"window": {
		"navigationBarHideCloseButton": true // true:隐藏右上角关闭按钮,false:显示右上角关闭按钮,默认为false。
	}
}
  1. 在页面的.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 是否支持打开体验版小程序?

答:支持。

平台支持为小程序配置体验版本和体验成员,拥有体验权限的成员能够打开体验版小程序。

使用体验版小程序的步骤:

  1. 在平台中上传小程序,并为小程序配置体验版
  2. 在初始化SDK的时候传入用户ID:
val config = FinAppConfig.Builder()
    .setUserId("用户ID")
    .build()

只有当传入的用户ID在小程序配置的成员列表中时,才能打开体验版小程序,否则在打开小程序的时候,前置页面中会提示“无体验权限”。

  1. 通过调用SDK提供的接口打开小程序,接口如下:
/**
 * 启动小程序
 *
 * @param context 上下文
 * @param startAppletDecryptRequest 请求体
 */
fun startApplet(context: Context, startAppletDecryptRequest: StartAppletDecryptRequest)`

StartAppletDecryptRequest的结构如下:

/**
 * 启动小程序请求实体类
 *
 * @param info 小程序加密信息
 */
data class StartAppletDecryptRequest(val info: String)

StartAppletDecryptRequestinfo字段表示小程序加密信息,和小程序体验版二维码中的info字段对应。因此打开体验版小程序时,StartAppletDecryptRequestinfo字段传小程序体验版二维码中的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()方法,显示自定义菜单即可。

  1. 通过调用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接口方法在小程序进程执行。

  1. 使用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的方法如下:

  1. 在Android工程最外层的build.gradle文件中添加jitpack仓库:

    allprojects {
        repositories {
            ... 其它仓库 ...
            maven { url "https://jitpack.io" }
          	... 其它仓库 ...
        }
    }
    
  2. 在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" />
    
  3. 在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代码,从而发生错误。

对于这类问题,一般建议:

  1. 在编码的时候,尽可能使用兼容性好的语法;
  2. 如果不可避免地需要使用一些新语法特性,可以尝试引入一些语法转换工具,如babel-polyfill等,将新的语法自动转换为低版本的语法,这样你就可以在使用新语法特性的时候不用考虑环境兼容的问题。

# 3.4 如何开启vConsole调试小程序?

如果需要使用vConsole调试小程序,目前有多种方式开启Debug模式。

  1. SDK初始化配置中的setEnableAppletDebug为true,可使所有小程序显示vconsole。请在应用上线时保持该配置为false状态。(设置为true时,无法在小程序内通过api方式关闭vConsole)。
  2. SDK初始化配置中的setEnableAppletDebug为false时,每个小程序可独立开启Debug模式。非线上版本(比如体验版、审核版、开发版、预览版)可在更多菜单中通过【打开调试】开启Debug模式。
  3. SDK初始化配置中的setEnableAppletDebug为false时,通过小程序调用api(ft.setEnableDebug)来独立开启Debug模式。

如果需要调试小程序,目前有多种方式开启vconsole,从而可以看到小程序中的日志。 开关vconsole,要从SDK和小程序两个方面来说。

SDK方面 SDK里有两个地方影响vconsole的开启和关闭:

  1. 初始化配置项FinAppConfig中的appletDebugMode参数;
  2. 更多面板里的 【打开调试】和【关闭调试】按钮。

appletDebugMode是个枚举类型,目前一共有四个值:

  • appletDebugModeUndefined:默认值。
  • appletDebugModeEnable:强制所有小程序(所有版本)均开启vconsole,并且更多面板里不显示【打开调试】和【关闭调试】按钮。
  • appletDebugModeDisable:类似微信的效果,小程序正式版更多面板里不显示【打开调试】和【关闭调试】按钮。但是非线上版 更多面板里会显示【打开调试】和【关闭调试】按钮。
  • appletDebugModeForbidden:强制所有小程序(所有版本)均不开启vconsole,并且更多面板里不显示【打开调试】和【关闭调试】按钮。

小程序方面 小程序里可以调用小程序api(ft.setEnableDebug)来开启vconsole。

但是,如果appletDebugMode设置为appletDebugModeEnable,则ft.setEnableDebug接口不生效。 如果appletDebugMode设置为appletDebugModeForbidden,则ft.setEnableDebug接口也不生效。

最后,建议在app开发阶段,设置appletDebugModeappletDebugModeEnable;然后在App提交审核时,修改为appletDebugModeDisable。 因为设置为appletDebugModeDisable,正式版小程序 不会显示 【打开调试】和【关闭调试】按钮,但是依然可以通过api(ft.setEnableDebug)来开启vconsole。 而非正式版小程序,可以通过【打开调试】和ft.setEnableDebug来开启vconsole。