Android scheme 跳转的设计与实现详解
缘起
随着App的成长,我们难免会遇到以下这些需求:
- H5跳原生界面
- Notification点击调相关界面
- 根据后台返回数据跳转界面,例如登录成功后跳不同界面或者根据运营需求跳不同界面
- 实现AppLink的跳转
为了解决这些问题,App一般都会自定义一个scheme跳转协议,多端都实现这个协议,以此来解决各种运营需求。今天就来解析下QMUI最新版QMUISchemeHandler的设计与实现。
一个scheme的格式大概是这样子:
schemeName://action?param1=value1¶m2=value2
例如:
qmui://home?tab=2
从技术角度来讲,实现scheme的跳转并不是件很难的事情,就是下面两个步骤:
- 解析scheme
- 根据解析结果跳转指定界面
但是写代码时如果不加以设计,就容易是堆一堆的ifelse。例如:
if(action=="action1"){ doAction1(params) }elseif(action=="action2"){ doAction2(params) }else{ ... }
每当有新的scheme添加时,就去添加一个if,直到它逐渐变成一段巨长的烂代码,改都改不动。因而我们要勤思考、多重构,尽早通过设计出优良的框架来解放自己的双手。
对于ifelse这类的重构,一个基本的方式就是用查表法,将所有的条件以及其所要执行的行为放在一个map里,然后使用时通过去查询这个map而获取要执行的行为。而我们可以通过注解配合代码生成的方式构建这个map,从而减少我们代码的编写量。除此之外,我们还需要考虑各种功能性需求:
- 可以设置拦截器interceptor,例如跳某些界面,如果是非登录的状态,可能需要跳转到登录界面
- 参数可以指定一些基础类型,scheme所携带的参数的值都是字符串,但我们希望它可以方便的转换成我们需要的基础类型
- 同一个action可以根据参数的不同而有不同的跳转行为,例如都是跳转书籍详情,漫画书籍和普通书籍要跳转的界面可能不一样
- 如果当前界面已经是目标界面,可以选择刷新当前界面或者启动一个新界面
- 对于QMUI,是同时支持Activity和Fragment的,因而scheme也要同时支持这两者
- 可以自定义新界面的实例化方法
接口设计
任何一个库的开发,为了让业务使用方足够舒心,既要保证库的功能足够强大,也要保证使用的方便性,QMUIScheme对外主要是QMUISchemeHandler这个入口类,以及ActivityScheme和FragmentScheme两个注解。
QMUISchemeHandler
QMUISchemeHandler通过Builder模式实例化:
//设置schemeName valinstance=QMUISchemeHandler.Builder("qmui://") //防止短时间类触发多次相同的scheme跳转 .blockSameSchemeTimeout(1000) //scheme参数decode .addInterpolator(newQMUISchemeParamValueDecoder()) .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@interfaceActivityScheme{ //schemeaction名 Stringname(); //必须的参数列表,用于支持同一个action对应多个scheme的场景,每一项可以是"type=4"来指定值,或者只传"type"来匹配任意值 String[]required()default{}; //如果当前界面就是scheme跳转的目标值,可以选择刷新当前界面,当然当前界面必须实现ActivitySchemeRefreshable booleanuseRefreshIfCurrentMatched()defaultfalse; //自定义当前scheme的匹配实现方法,传值为QMUISchemeMatcher的实现 Class>customMatcher()defaultvoid.class; //自定义当前Activity实例工厂,传值为QMUISchemeIntentFactory Class>customFactory()defaultvoid.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@interfaceFragmentScheme{ //这些参数都同ActivityScheme Stringname(); String[]required()default{}; Class>customMatcher()defaultvoid.class; String[]keysWithIntValue()default{}; String[]keysWithBoolValue()default{}; String[]keysWithLongValue()default{}; String[]keysWithFloatValue()default{}; String[]keysWithDoubleValue()default{}; //同ActivityScheme,但当前UI必须实现FragmentSchemeRefreshable booleanuseRefreshIfCurrentMatched()defaultfalse; //同ActivityScheme,但传值是QMUISchemeFragmentFactory的实现类 Class>customFactory()defaultvoid.class; //可以承载目标Fragment的activity列表,如果当前activity不在列表里,则用activities的第一项启动新的activity Class>[]activities(); //是否强制启动新的Activity booleanforceNewActivity()defaultfalse; //可以通过scheme里的参数来控制是否强制启动新的Activity StringforceNewActivityKey()default""; }
可以看出,我们前面所罗列的各种需求,都在SchemeHandler以及两个scheme里体现出来了。
使用
对于业务使用者,我们只需要在Activity或者Fragment上加上注解。QMUISchemeHandler默认会将参数解析出来并放到Activity的intent里或者Fragment的arguments里,因而我们可以在onCreate里将我们关心的值取出来:
@ActivityScheme(name="activity1") classActivity1:QMUIActivity{ overridefunonCreate(...){ ... if(isStartedByScheme()){ //通过intentextra获取参数的值 valparam1=getIntent().getStringExtra(paramName) } } } @FragmentScheme(name="activity1",activities={QDMainActivity.class}) classFragment1:QMUIFragment{ overridefunonCreate(...){ ... if(isStartedByScheme()){ //通过arguments获取参数的值 valparam1=getArguments().getString(paramName) } } }
这种传值方法很符合Android官方设计的做法了,这也要求Fragment遵循无参构造器的使用方式。
对于WebView,我们可以通过重写WebViewClient#shouldOverrideUrlLoading来处理scheme跳转:
classMyWebViewClient:WebViewClient{ overridefunshouldOverrideUrlLoading(view:WebView,url:String){ if(schemeHandler.handle(url)){ returntrue; } returnsuper.shouldOverrideUrlLoading(view,url); } overridefunshouldOverrideUrlLoading(view:WebView,request:WebResourceRequest){ if(schemeHandler.handle(request.getUrl().toString())){ returntrue; } returnsuper.shouldOverrideUrlLoading(view,request); } }
实现
QMUISchemeHandler采用代码生成的方式,在编译期生成一个SchemeMapImpl类,其实现了SchemeMap类
publicinterfaceSchemeMap{ //通过action和参数寻找SchemeItem SchemeItemfindScheme(QMUISchemeHandlerhandler,StringschemeAction,Mapparams); //判断schemeAction是否存在 booleanexists(QMUISchemeHandlerhandler,StringschemeAction); }
而每个scheme的注解对应一个SchemeItem:
- ActivityScheme对应实例化一个ActivitySchemeItem类,并加入到map中
- FragmentScheme对应实例化一个FragmentSchemeItem类,并加入到map中
在编译期通过SchemeProcessor生成的SchemeMapImpl大概是这样子的:
publicclassSchemeMapImplimplementsSchemeMap{ privateMap>mSchemeMap; publicSchemeMapImpl(){ mSchemeMap=newHashMap<>(); List elements; ArrayMap required=null; elements=newArrayList<>(); required=null; elements.add(newFragmentSchemeItem(QDSliderFragment.class,false,newClass[]{QDMainActivity.class},null,false,"",required,null,null,null,null,null,SliderSchemeMatcher.class)); mSchemeMap.put("slider",elements); elements=newArrayList<>(); required=newArrayMap<>(); required.put("aa",null); required.put("bb","3"); elements.add(newActivitySchemeItem(ArchTestActivity.class,true,null,required,null,newString[]{"aa"},null,null,null,null)); mSchemeMap.put("arch",elements); } @Override publicSchemeItemfindScheme(QMUISchemeHandlerarg0,Stringarg1,Map arg2){ List list=mSchemeMap.get(arg1); if(list==null||list.isEmpty()){ returnnull; } for(inti=0;i 整体的设计以及实现思路就是这样,剩下的就是各种编码细节了。有兴趣的可以通过QMUISchemeHandler#handle()进行追踪下,或者看看SchemeProcessor是如何做代码生成的。这个功能看上去简单,其实也包括了Builder模式、责任链模式、工厂方法等设计模式的运用,还有SchemeMatcher、SchemeItem等对面向对象的接口、继承、多态等的运用。读一读或许对你有所启迪,或许你也能帮我发现某些潜在的Bug。
总结
到此这篇关于Androidscheme跳转的设计与实现的文章就介绍到这了,更多相关Androidscheme跳转的设计与实现内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!