一、为什么要在项目中引入路由?
在开始之前我们先来思考一下这个问题。为什么要在项目中引入路由?相信大家的答案可能会有所不同,但是应该也不外乎以下几点:
1.为了实现项目组件化
想必很多开发者引入路由的目的都是因为要实现项目组件化。我们知道,组件化的项目各个业务模块之间没有相互的依赖关系。不同业务模块之间的通信最好的解决方案就是支持页面路由。
2.方便APP内部跳转
可能有些小伙伴会有疑问,App内部直接通过Intent跳转不是很好吗,为什么要多此一举引入路由呢?当然,通常情况下通过Intent跳转也无伤大雅。但是在某些情况下,比如像下图这样的一个页面:
这是一个典型的多Type的RecyclerView页面,这个页面中所有的数据都是从服务器获取的,在引入路由之前所有的点击跳转事件都需要后台给我们一个type,我们根据type判断需要向哪一个Activity跳转,并且需要通过Intent携带目的页面所需要的参数。显然这样写会使我们代码变得非常臃肿,代码之间的耦合度也非常高。然而在引入路由之后一切都变得不一样了。我们只需要后台返回目的页面所对应的URL,并在URL上拼接页面跳转所需要的参数,此时前台只需要拿到URL,然后通过路由即可到达对应的页面。这样以来使我们的代码变得简洁明了,并且保证了代码的低耦合。
3.方便APP外部跳转
通常可以看到很多应用支持从浏览器唤醒App并跳转到对应的页面。做到比较好的如知乎,体验过知乎的小伙伴应该知道,知乎可以从浏览器唤醒App并且直接在App中打开当前在浏览器中浏览的内容。我们知道,从外部唤起App需要给Activity添加Schema。而如果App内部有许多Activity需要支持外部唤起,我们不可能为这些Activity都添加Schema。那么此时我们就可以单独设置一个支持Schema的Activity,浏览器可以通过Schema唤起这个Activity。而在这个Activity中会接收浏览器传过来的URL,然后根据URL进行路由分发,通过URL路由到对应的页面即可。
二 、ARouter的使用
其实很不想在这篇文章中长篇大论如何使用ARouter,因为ARouter的官方文档上已经非常详细的告诉了开发者如何去使用,只要仔细的阅读ARouter的文档基本上绝大部分问题都可以得到解决。但是为了照顾没有使用过ARouter的小伙伴,这里还是再啰嗦一下。如果你对ARouter的使用已经非常熟悉了那么你可以忽略此章节,直接到下一章了。
1.添加依赖和配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| android { defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] } } } }
dependencies { // 替换成最新版本, 需要注意的是api // 要与compiler匹配使用,均使用最新版可以保证兼容 implementation 'com.alibaba:arouter-api:x.x.x' annotationProcessor 'com.alibaba:arouter-compiler:x.x.x' ... }
|
这里需要注意,如果你的项目有多个业务模块,那么每个模块都需要在gradle中添加以上配置。
2.初始化SDK
1 2 3 4 5
| if (isDebug()) { ARouter.openLog(); ARouter.openDebug(); } ARouter.init(mApplication);
|
3.添加注解
1 2 3 4 5 6
|
@Route(path = "/test/activity") public class YourActivity extend Activity { ... }
|
4.发起路由操作
1 2 3 4 5 6 7 8 9
| ARouter.getInstance().build("/test/activity").navigation();
ARouter.getInstance().build("/test/1") .withLong("key1", 666L) .withString("key3", "888") .withObject("key4", new Test("Jack", "Rose")) .navigation();
|
很多情况下需要通过URL跳转,ARouter支持直接通过URL跳转:
1 2
| Uri uri= Uri.parse(url); ARouter.getInstance().build(uri).navigation();
|
5.路由解析参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
@Route(path = "/test/activity") public class Test1Activity extends Activity { @Autowired public String name; @Autowired int age; @Autowired(name = "girl") boolean boy; @Autowired TestObj obj; @Autowired List<TestObj> list; @Autowired Map<String, List<TestObj>> map; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ARouter.getInstance().inject(this);
Log.d("param", name + age + boy); } }
@Route(path = "/yourservicegroupname/json") public class JsonServiceImpl implements SerializationService { @Override public void init(Context context) {
}
@Override public <T> T json2Object(String text, Class<T> clazz) { return JSON.parseObject(text, clazz); }
@Override public String object2Json(Object instance) { return JSON.toJSONString(instance); } }
|
除了使用@Autowired注解注入参数外,还可以与普通页面跳转一样通过getIntent()获取参数。
以上就是ARouter的一些基本用法,了解这些基本用法之后并不等于已经掌握了ARouter。因为当你实际用到项目中的时候可能会面临诸多问题。
三 、ARouter的采坑之路
如果你只是简单的写一个ARouter使用的Demo,那么可能上一章的内容已经足够了。但是当你在项目中引入ARouter后各种各样的问题便会接踵而至。
1.使用ARouter实现登录拦截
这是在项目中引入ARouter后面临的第一个问题。通常情况下,大部分App不登录便可以进入主页面,在跳转需要用户权限的页面时会首先跳转到登录页面引导用户登录。我相信大部分的开发在最初时候都写过类似这样的代码:
1 2 3 4 5
| if (isLogin) { goToDestination(); } else { goToLogin(); }
|
在每次跳转页面的时候都需要进行是否登录的判断,这样的代码显然有很大的弊端。而ARouter为我们提供了面向切面的登录拦截功能,ARouter的文档上给了我们一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@Interceptor(priority = 8, name = "测试用拦截器") public class TestInterceptor implements IInterceptor { @Override public void process(Postcard postcard, InterceptorCallback callback) { ... callback.onContinue(postcard);
}
@Override public void init(Context context) { } }
|
如果你按着官方文档上这样写,那么你大概率会碰到很多问题。列举如下:
如何处理有些页面需要登录拦截,有些页面不需要登录拦截?
如果你添加了拦截器,那么在每次路由跳转时都会优先走到拦截器中,在拦截器的process()方法中你可以通过判断当前是否登录来决定是否继续该路由操作,如果已经登录,那么直接通过 callback.onContinue(postcard)继续当前路由,而如果没有登录,那么就将目的页面修改为登录页。但是,不要忘了,添加拦截器后所有的路由操作都会优先走到这里,而我们的需求是只有需要用户权限的时候才需要跳转到登录页,否则即使没有登录依然可以跳转到目的页。此时我们应该怎么办?
如果你仔细的看了ARouter的开发文档,你可能注意到在@Route的注解有一个int类型的extras参数。如此我们便可以通过这个参数来对Activity进行标记是否需要登录:
1 2
| @Route(path = PATH_TEST, extras = IGNORE_LOGIN) public class TestActivity extends BaseTitleCompatActivity {}
|
接下来,在拦截器中可以拿到extras参数,以此来确定该页面是否需要登录:
1 2 3 4 5 6 7 8
| if(UserInfoTools.isLogin() || IGNORE_LOGIN == postcard.getExtra()) { callback.onContinue(postcard); } else { ARouter.getInstance().build(RoutingTable.PATH_GUEST_LOGIN).navigation(); ... }
|
到这里这个问题解决了,但是当你兴致勃勃的运行起来App,在未登录的情况下点击跳转到需要用户权限的页面,你憧憬着跳转页面会被拦截到登录页,但是你又被无情的事实打脸了。竟然页面毫无反应?于是你断点、打Log发现ARouter.getInstance().build(RoutingTable.PATH_GUEST_LOGIN).navigation()这句代码确实执行了,但是为什么没有跳转到登录页?于是你苦思冥想,突然灵光一闪,哇!是因为这一句路由也会走到了拦截器里,如此岂不成了一个死循环。于是你Google如何解决,发现原来需要调用greenChannel()来避免出现死循环。于是有了如下代码:
1 2 3 4 5 6 7 8
| if(UserInfoTools.isLogin() || IGNORE_LOGIN == postcard.getExtra()) { callback.onContinue(postcard); } else { ARouter.getInstance().build(RoutingTable.PATH_GUEST_LOGIN).greenChannel().navigation(); ... }
|
修改之后你怀着和刚才一样的心情兴致勃勃的运行起来App,心想,这次一定没问题。好!点击按钮….竟然成功跳转到了登录页面。于是你兴奋起来,疯狂的点击这些页面,发现都没问题。可是…当你点了几次之后突然发现,页面跳转无效了!!你简直不敢相信自己的眼睛,刚才明明是好好的…于是你在此陷入了沉思。
好吧,这次直接公布答案了,那是因为你需要将原来的路由打断,而之所以前几次有效大概猜测是因为greenChannel()去开启了多个channel,而ARouter的channel是有限的,因此在点击几次之后路由再次失效了。于是修改后代码如下:
1 2 3 4 5 6 7 8
| if(UserInfoTools.isLogin() || IGNORE_LOGIN == postcard.getExtra()) { callback.onContinue(postcard); } else { ARouter.getInstance().build(RoutingTable.PATH_GUEST_LOGIN).greenChannel().navigation(); callback.onInterrupt(null); }
|
关于登录拦截看似简单,实则使用时候竟然会碰到这么多问题!相信第一次使用时都会被虐的掉眼泪。
2.处理一个Activity对应多个路径的情况
在某些情况可能出现一个页面对应多个路径的情况。出现这种情况的原因可能是前期路由没有规划好,导致后边版本的路由路径做了修改。从而出现了一个Activity对应多个页面的情况。为了兼容旧版路由,我们不得不处理这种情况。但是,Route的注解中path是唯一的,并不能通过@Route注解解决一个Activity对应多个路径的情况。此时就需要用到ARouter的重写URL的功能。只需要实现PathReplaceService 接口,在重写的方法中对URI或者Path进行替换即可,注意,这个类一定要加@Route注解。代码参考如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Route(path = "/lost/service") public class ARouterLostReplaceService implements PathReplaceService { @Override public String forString(String path) { return path; }
@Override public Uri forUri(Uri uri) { String path = uri.getPath(); if(PATH_LOST1.equals(path)) { uri = replaceUriPath(uri, PATH_REAL1); } else if(PATH_LOST2.equals(path)) { uri = replaceUriPath(uri, PATH_REAL2); } return uri; }
@Override public void init(Context context) {
}
private Uri replaceUriPath(Uri uri, String path) { StringBuilder resultUrl = new StringBuilder(uri.getScheme() + "://" + uri.getHost() + path); String[] split = uri.toString().split("\\?"); if(split.length >= 2) { resultUrl.append("?").append(split[1]); } return Uri.parse(resultUrl.toString()); } }
|
3.ARouter全局降级策略
在路由跳转时可能会出现找不到Path对应页面的情况,对于这种情况可以通过实现DegradeService 接口来处理,同样这个类也必须要添加@Route注解。这样当路由跳转时找不到路径就会走到这个类的onLost方法中,此时就可以在这个方法中来做相应的处理了。
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Route(path = "/lost/path") public class DegradeServiceImpl implements DegradeService { @Override public void onLost(Context context, Postcard postcard) { }
@Override public void init(Context context) {
} }
|
四、通过浏览器跳转到App对应页面
1.Schema协议
很多人对于Schema协议比较陌生,但是如果说URL大家一定都非常熟悉。其实URL就是一种Schema协议。Schema协议通常由四部分组成:
1 2 3 4 5
| :///? scheme:表示协议名称 host:Schema所作用的地址域 path:Schema指定的路径 query:携带的参数
|
拿百度搜索的URL来举例子:https://www.baidu.com/s?wd=要搜索的关键字。这个URL与Schema协议的对应关系如下
schema::https
host: www.baidu.com
path: /s
query:wd=要搜索的关键字
了解了Schema协议后,其实我们完全可以按照Schema协议的格式来自定义一个Schema链接,如下:
myApp://www.myApp.com/main/home?id=1
我们自己定义的Schema链接的对应关系为:
schema::myApp
host:www.myApp.com
path:/main/home
query:id=1
2.通过Schema链接打开Activity
通过浏览器打开App其实就是通过Schema链接来实现的。我们就以上一节中自定义的Schema链接为例来实现浏览器打开App。首先在项目中添加一个RouterActivity,RouterActivity在AndroidManifest中的配置如下:
1 2 3 4 5 6 7 8 9 10 11
| <activity android:name=".activity.RouterActivity" android:configChanges="orientation|keyboardHidden|screenSize" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="myApp" /> </intent-filter> </activity>
|
我们在AndroidManifest中为RouterActivity添加了schema,此时在HTML中写入以下代码:
1
| <a href="myApp://www.myApp.com/main/home?id=1">打开APP</a>
|
通过点击HTML页面的”打开App”便可启动RouterActivity。并且RouterActivity启动后可以通过Intent获取到启动的URI。代码如下:
1 2 3 4 5 6 7 8
| #RouterActivity @Override protected void onCreate(Bundle data) { super.onCreate(data); Uri launchUri = getIntent().getData(); dispatchRouterUri(launchUri); }
|
至此,我们已经可以通过App来打开项目的RouterActivity。
3.通过路由跳转到目的页面
上一节中我们通过HTML打开了RouterActivity,并在RouterActivity中拿到了跳转的URI,那么接下来我们便可以根据URI的信息打开对应的页面了。但是在开启路由跳转之前为了保险起见需要对URI进行一些校验。详细代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| private void dispatchRouterUri(Uri launchUri) { if(RoutingTable.isValidRouterUri(launchUri)) { if(App.isRootActivityLaunched()) { if(RoutingTable.isWxUri(launchUri)) { RoutingTable.openMiniProgram(this, launchUri); finish(); return; } ARouter.getInstance().build(launchUri).navigation(); } else { SharedPreferUtil.put(Constants.ROUTER_URI, launchUri.toString()); launchApp(); } } else { launchApp(); } finish(); }
|
上面代码中,我们对URI做了一系列校验,根据不同的URI做不同的处理。同时我们应该也注意到了,如果APP已经启动了,那么就可以直接跳转对应的页面了,而如果App没有启动,那么则是先将URI保存到了SharedPreference中,接着启动了App。那么此时App启动后会在MainActivity中读取SharedPreference中的配置,如果读取到URI的信息,那么就先将此数据从SharedPreference中移除,然后通过ARouter跳转到URI指定的页面去。MainActivity中的部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #MainActivity
private void resumeRoute() { String interruptedLaunchUriString = Configuration.get(Constants.INTERRUPTED_ROUTER_URI, null); SharedPreferUtil.remove(Constants.ROUTER_URI); Uri launchUri = null; if(interruptedLaunchUriString != null) { launchUri = Uri.parse(interruptedLaunchUriString); }
if(launchUri == null) { return; } ARouter.getInstance().build(launchUri).navigation(); }
|
关于ARouter的路由方案所涉及的内容至此已经全部讲完了。