38

Android scheme 跳转的设计与实现

 3 years ago
source link: http://blog.cgsdream.org/2020/06/08/scheme_design_and_impl/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

缘起

随着 App 的成长,我们难免会遇到以下这些需求:

  1. H5 跳原生界面
  2. Notification 点击调相关界面
  3. 根据后台返回数据跳转界面,例如登录成功后跳不同界面或者根据运营需求跳不同界面
  4. 实现 AppLink 的跳转

为了解决这些问题,App 一般都会自定义一个 scheme 跳转协议,多端都实现这个协议,以此来解决各种运营需求。今天就来解析下 QMUI 最新版 QMUISchemeHandler 的设计与实现。

一个 scheme 的格式大概是这样子:

schemeName://action?param1=value1&param2=value2

例如:

qmui://home?tab=2

从技术角度来讲,实现 scheme 的跳转并不是件很难的事情,就是下面两个步骤:

  1. 解析 scheme
  2. 根据解析结果跳转指定界面

但是写代码时如果不加以设计,就容易是堆一堆的 if else。例如:

if(action=="action1"){  
    doAction1(params)
}else if(action=="action2"){
    doAction2(params)
}else {
    ...
}

每当有新的 scheme 添加时,就去添加一个 if,直到它逐渐变成一段巨长的烂代码,改都改不动。因而我们要勤思考、多重构,尽早通过设计出优良的框架来解放自己的双手。

对于 if else 这类的重构,一个基本的方式就是用查表法,将所有的条件以及其所要执行的行为放在一个 map 里,然后使用时通过去查询这个 map 而获取要执行的行为。而我们可以通过注解配合代码生成的方式构建这个 map,从而减少我们代码的编写量。除此之外,我们还需要考虑各种功能性需求:

  1. 可以设置拦截器 interceptor,例如跳某些界面,如果是非登录的状态,可能需要跳转到登录界面
  2. 参数可以指定一些基础类型, scheme 所携带的参数的值都是字符串,但我们希望它可以方便的转换成我们需要的基础类型
  3. 同一个 action 可以根据参数的不同而有不同的跳转行为,例如都是跳转书籍详情,漫画书籍和普通书籍要跳转的界面可能不一样
  4. 如果当前界面已经是目标界面,可以选择刷新当前界面或者启动一个新界面
  5. 对于 QMUI,是同时支持 Activity 和 Fragment 的,因而 scheme 也要同时支持这两者
  6. 可以自定义新界面的实例化方法

接口设计

任何一个库的开发,为了让业务使用方足够舒心,既要保证库的功能足够强大,也要保证使用的方便性,QMUI Scheme 对外主要是 QMUISchemeHandler 这个入口类, 以及 ActivitySchemeFragmentScheme 两个注解。

QMUISchemeHandler

QMUISchemeHandler 通过 Builder 模式实例化:

// 设置schemeName
val instance = QMUISchemeHandler.Builder("qmui://")  
    // 防止短时间类触发多次相同的scheme跳转
    .blockSameSchemeTimeout(1000)
    // scheme 参数 decode
    .addInterpolator(new QMUISchemeParamValueDecoder())
    .addInterpolator(...)
    // 默认 fragment 实例化 factory
    .defaultFragmentFactory(...)
    // 默认 activity 实例化 factory
    .defaultIntentFactory(...)
    // 默认 scheme 匹配器
    .defaultSchemeMatcher(...)
    .build();

if(!instance.handle("qmui://xxx")){  
  // scheme 未被 handle,日志记录?
}

大多数场景, QMUISchemeHandler 采用单例模式即可。 其可以设置多个拦截器、设置 fragment、activity 的默认实例化工厂、以及默认的匹配器。实例工厂和匹配器都是提供了默认实现的,大多数场景是不需要调用者关心的。而且这里都只是设置全局默认值,到了 scheme 注解那一层,还可以为每个 scheme 指定不同的值,以满足可能的自定义需求。

ActivityScheme 与 FragmentScheme 注解

这两个注解是非常相似的,但是因为 Fragment 有一些更多的配置项,因为独立出来了。

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface ActivityScheme {  
    // scheme action 名
    String name();
    // 必须的参数列表,用于支持同一个 action 对应多个 scheme 的场景,每一项可以是"type=4" 来指定值,或者只传"type"来匹配任意值
    String[] required() default {};
    // 如果当前界面就是 scheme 跳转的目标值,可以选择刷新当前界面,当然当前界面必须实现 ActivitySchemeRefreshable
    boolean useRefreshIfCurrentMatched() default false;
    // 自定义当前 scheme 的匹配实现方法, 传值为 QMUISchemeMatcher 的实现
    Class<?> customMatcher() default void.class;
    // 自定义当前 Activity 实例工厂,传值为 QMUISchemeIntentFactory
    Class<?> customFactory() default void.class;
    // 指定参数的类型,支持 int/bool/long/float/double 这些基础类型,不指定则为 string 类型
    String[] keysWithIntValue() default {};
    String[] keysWithBoolValue() default {};
    String[] keysWithLongValue() default {};
    String[] keysWithFloatValue() default {};
    String[] keysWithDoubleValue() default {};
}


@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface FragmentScheme {  
    // 这些参数都同 ActivityScheme
    String name();
    String[] required() default {};
    Class<?> customMatcher() default void.class;
    String[] keysWithIntValue() default {};
    String[] keysWithBoolValue() default {};
    String[] keysWithLongValue() default {};
    String[] keysWithFloatValue() default {};
    String[] keysWithDoubleValue() default {};

    //同 ActivityScheme,但当前UI必须实现 FragmentSchemeRefreshable
    boolean useRefreshIfCurrentMatched() default false;

    // 同 ActivityScheme, 但传值是 QMUISchemeFragmentFactory 的实现类
    Class<?> customFactory() default void.class;
    // 可以承载目标 Fragment 的 activity 列表,如果当前 activity 不在列表里,则用 activities 的第一项启动新的 activity
    Class<?>[] activities();
    // 是否强制启动新的 Activity
    boolean forceNewActivity() default false;
    // 可以通过 scheme 里的参数来控制是否强制启动新的 Activity
    String forceNewActivityKey() default "";    
}

可以看出,我们前面所罗列的各种需求,都在 SchemeHandler 以及两个 scheme 里体现出来了。

使用

对于业务使用者,我们只需要在 Activity 或者 Fragment 上加上注解。 QMUISchemeHandler 默认会将参数解析出来并放到 Activity 的 intent 里或者 Fragment 的 arguments 里,因而我们可以在 onCreate 里将我们关心的值取出来:

@ActivityScheme(name="activity1")
class Activity1: QMUIActivity{

  override fun onCreate(...){
    ...
    if(isStartedByScheme()){
       // 通过 intent extra 获取参数的值
       val param1 = getIntent().getStringExtra(paramName)
    }
  }
}

@FragmentScheme(name="activity1", activities = {QDMainActivity.class})
class Fragment1: QMUIFragment{  
  override fun onCreate(...){
    ...
    if(isStartedByScheme()){
       // 通过 arguments 获取参数的值
       val param1 = getArguments().getString(paramName)
    }
  }
}

这种传值方法很符合 Android 官方设计的做法了,这也要求 Fragment 遵循无参构造器的使用方式。

对于 WebView, 我们可以通过重写 WebViewClient#shouldOverrideUrlLoading 来处理 scheme 跳转:

class MyWebViewClient: WebViewClient{  
    override fun shouldOverrideUrlLoading(view: WebView, url: String){
        if(schemeHandler.handle(url)){
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }

    override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest){
        if(schemeHandler.handle(request.getUrl().toString())){
            return true;
        }
        return super.shouldOverrideUrlLoading(view, request);
    }
}

实现

QMUISchemeHandler 采用代码生成的方式,在编译期生成一个 SchemeMapImpl 类,其实现了 SchemeMap

public interface SchemeMap {

    // 通过 action 和参数寻找 SchemeItem
    SchemeItem findScheme(QMUISchemeHandler handler, String schemeAction, Map<String, String> params);
    // 判断 schemeAction 是否存在
    boolean exists(QMUISchemeHandler handler, String schemeAction);
}

而每个 scheme 的注解对应一个 SchemeItem :

  • ActivityScheme 对应实例化一个 ActivitySchemeItem 类,并加入到 map 中
  • FragmentScheme 对应实例化一个 FragmentSchemeItem 类,并加入到 map 中

在编译期通过 SchemeProcessor 生成的 SchemeMapImpl 大概是这样子的:

public class SchemeMapImpl implements SchemeMap {  
  private Map<String, List<SchemeItem>> mSchemeMap;

  public SchemeMapImpl() {
    mSchemeMap = new HashMap<>();
    List<SchemeItem> elements;
    ArrayMap<String, String> required = null;
    elements = new ArrayList<>();
    required =null;
    elements.add(new FragmentSchemeItem(QDSliderFragment.class,false,new Class[]{QDMainActivity.class},null,false,"",required,null,null,null,null,null,SliderSchemeMatcher.class));
    mSchemeMap.put("slider", elements);

    elements = new ArrayList<>();
    required = new ArrayMap<>();
    required.put("aa", null);
    required.put("bb", "3");
    elements.add(new ActivitySchemeItem(ArchTestActivity.class,true,null,required,null,new String[]{"aa"},null,null,null,null));
    mSchemeMap.put("arch", elements);

  }

  @Override
  public SchemeItem findScheme(QMUISchemeHandler arg0, String arg1, Map<String, String> arg2) {
    List<SchemeItem> list = mSchemeMap.get(arg1);
    if(list == null || list.isEmpty()) {
      return null;
    }
    for (int i = 0; i < list.size(); i++) {
      SchemeItem item = list.get(i);
      if(item.match(arg0, arg2)) {
        return item;
      }
    }
    return null;
  }

  @Override
  public boolean exists(QMUISchemeHandler arg0, String arg1) {
    return mSchemeMap.containsKey(arg1);
  }
}

整体的设计以及实现思路就是这样,剩下的就是各种编码细节了。有兴趣的可以通过 QMUISchemeHandler#handle() 进行追踪下,或者看看 SchemeProcessor 是如何做代码生成的。这个功能看上去简单,其实也包括了 Builder 模式、责任链模式、工厂方法等设计模式的运用,还有 SchemeMatcher、 SchemeItem 等对面向对象的接口、继承、多态等的运用。读一读或许对你有所启迪,或许你也能帮我发现某些潜在的 Bug。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK