基于Arouter的路由实现方案

2,845 阅读15分钟

这本是一篇应该写在去年的文章,但仅仅因为...懒,这篇文章在草稿箱里静静的躺了一年多,被无限期推迟到了现在。最近刚好完成了公司项目的路由改造,借此机会来对这篇文章做一个了结。

一、为什么要在项目中引入路由?

在开始之前我们先来思考一下这个问题。为什么要在项目中引入路由?相信大家的答案可能会有所不同,但是应该也不外乎以下几点:

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.添加依赖和配置

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

if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
    ARouter.openLog();     // 打印日志
    ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

3.添加注解

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}

4.发起路由操作

// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/test/activity").navigation();

// 2. 跳转并携带参数
ARouter.getInstance().build("/test/1")
            .withLong("key1", 666L)
            .withString("key3", "888")
            .withObject("key4", new Test("Jack", "Rose"))
            .navigation();

很多情况下需要通过URL跳转,ARouter支持直接通过URL跳转:

 Uri uri= Uri.parse(url);
 ARouter.getInstance().build(uri).navigation();

5.路由解析参数

// 为每一个参数声明一个字段,并使用 @Autowired 标注
// URL中不能传递Parcelable类型数据,通过ARouter api可以传递Parcelable对象
@Route(path = "/test/activity")
public class Test1Activity extends Activity {
    @Autowired
    public String name;
    @Autowired
    int age;
    
    // 通过name来映射URL中的不同参数
    @Autowired(name = "girl") 
    boolean boy;
    
    // 支持解析自定义对象,URL中使用json传递
    @Autowired
    TestObj obj;      
    
    // 使用 withObject 传递 List 和 Map 的实现了
    // Serializable 接口的实现类(ArrayList/HashMap)
    // 的时候,接收该对象的地方不能标注具体的实现类类型
    // 应仅标注为 List 或 Map,否则会影响序列化中类型
    // 的判断, 其他类似情况需要同样处理        
    @Autowired
    List<TestObj> list;
    @Autowired
    Map<String, List<TestObj>> map;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ARouter.getInstance().inject(this);

    // ARouter会自动对字段进行赋值,无需主动获取
    Log.d("param", name + age + boy);
    }
}


// 如果需要传递自定义对象,新建一个类(并非自定义对象类),然后实现 SerializationService,并使用@Route注解标注(方便用户自行选择序列化方式),例如:
@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不登录便可以进入主页面,在跳转需要用户权限的页面时会首先跳转到登录页面引导用户登录。我相信大部分的开发在最初时候都写过类似这样的代码:

 if (isLogin) {
     goToDestination();
  } else {
     goToLogin();
  }

在每次跳转页面的时候都需要进行是否登录的判断,这样的代码显然有很大的弊端。而ARouter为我们提供了面向切面的登录拦截功能,ARouter的文档上给了我们一个例子:

// 比较经典的应用就是在跳转过程中处理登陆事件,这样就不需要在目标页重复做登陆检查
// 拦截器会在跳转之间执行,多个拦截器会按优先级顺序依次执行
@Interceptor(priority = 8, name = "测试用拦截器")
public class TestInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
    ...
    callback.onContinue(postcard);  // 处理完成,交还控制权
    // callback.onInterrupt(new RuntimeException("我觉得有点异常"));      // 觉得有问题,中断路由流程

    // 以上两种至少需要调用其中一种,否则不会继续路由
    }

    @Override
    public void init(Context context) {
    // 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次
    }
}

如果你按着官方文档上这样写,那么你大概率会碰到很多问题。列举如下: 如何处理有些页面需要登录拦截,有些页面不需要登录拦截? 如果你添加了拦截器,那么在每次路由跳转时都会优先走到拦截器中,在拦截器的process()方法中你可以通过判断当前是否登录来决定是否继续该路由操作,如果已经登录,那么直接通过 callback.onContinue(postcard)继续当前路由,而如果没有登录,那么就将目的页面修改为登录页。但是,不要忘了,添加拦截器后所有的路由操作都会优先走到这里,而我们的需求是只有需要用户权限的时候才需要跳转到登录页,否则即使没有登录依然可以跳转到目的页。此时我们应该怎么办? 如果你仔细的看了ARouter的开发文档,你可能注意到在@Route的注解有一个int类型的extras参数。如此我们便可以通过这个参数来对Activity进行标记是否需要登录:

@Route(path = PATH_TEST, extras = IGNORE_LOGIN)
public class TestActivity extends BaseTitleCompatActivity {}

接下来,在拦截器中可以拿到extras参数,以此来确定该页面是否需要登录:

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()来避免出现死循环。于是有了如下代码:

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是有限的,因此在点击几次之后路由再次失效了。于是修改后代码如下:

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注解。代码参考如下:

@Route(path = "/lost/service")
public class ARouterLostReplaceService implements PathReplaceService {
    @Override
    public String forString(String path) {	//	对于path处理与uri类似
        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) {

    }

    /**
     * 替换URI中的path
     * 
     * @param uri 被替换的uri
     * @param path 要替换的path
     * @return 替换后的uri
     */
    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方法中,此时就可以在这个方法中来做相应的处理了。

// 实现DegradeService接口,并加上一个Path内容任意的注解即可
@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协议通常由四部分组成:

 [scheme]://[host]/[path]?[query]
 scheme:表示协议名称
 host:Schema所作用的地址域
 path:Schema指定的路径
 query:携带的参数

拿百度搜索的URL来举例子:www.baidu.com/s?wd=要搜索的关键…

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中的配置如下:

<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中写入以下代码:

<a href="myApp://www.myApp.com/main/home?id=1">打开APP</a>

通过点击HTML页面的"打开App"便可启动RouterActivity。并且RouterActivity启动后可以通过Intent获取到启动的URI。代码如下:

   #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进行一些校验。详细代码如下:

private void dispatchRouterUri(Uri launchUri) {
        if(RoutingTable.isValidRouterUri(launchUri)) {  //	判断是否是合法的URI,这里只有URI携带了Path才算合法
            if(App.isRootActivityLaunched()) { // app已启动
                if(RoutingTable.isWxUri(launchUri)) { //	如果是微信的URI那么目的地是要跳转到小程序的(此处为项目中的需求)
                    RoutingTable.openMiniProgram(this, launchUri);
                    finish();
                    return;
                }
                //	通过ARouter路由到目的页面
               ARouter.getInstance().build(launchUri).navigation();
            } else {  // app未启动, 保存router uri, 幷尝试启动app
               SharedPreferUtil.put(Constants.ROUTER_URI, launchUri.toString());
               launchApp();
            }
        } else {  //	 走到此处可能是因为URI没有携带Path,即并非要跳转目的页面,而是要启动APP  。因此直接启动App即可
            launchApp();
        }
        finish();
    }

上面代码中,我们对URI做了一系列校验,根据不同的URI做不同的处理。同时我们应该也注意到了,如果APP已经启动了,那么就可以直接跳转对应的页面了,而如果App没有启动,那么则是先将URI保存到了SharedPreference中,接着启动了App。那么此时App启动后会在MainActivity中读取SharedPreference中的配置,如果读取到URI的信息,那么就先将此数据从SharedPreference中移除,然后通过ARouter跳转到URI指定的页面去。MainActivity中的部分代码如下:

#MainActivity

private void resumeRoute() {
        // Continue for interrupted router uri
        String interruptedLaunchUriString =
                SharedPreferUtil.get(Constants.ROUTER_URI, null);
         //	移除SharedPreference中的URI,避免下次打开MainActivity错误跳转
        SharedPreferUtil.remove(Constants.ROUTER_URI);
        Uri launchUri = null;
        if(interruptedLaunchUriString != null) { // Activity未启动的情况下 通过外部Scheme跳转非MainActivity
            launchUri = Uri.parse(interruptedLaunchUriString);
        }

        if(launchUri == null) {
            return;
        }
		//	通过路由跳转到URI对应的页面
        ARouter.getInstance().build(launchUri).navigation();
    }

关于ARouter的路由方案所涉及的内容至此已经全部讲完了。

好库推荐

给大家推荐一下BannerViewPager。这是一个基于ViewPager实现的具有强大功能的无限轮播库。通过BannerViewPager可以实现腾讯视频、QQ音乐、酷狗音乐、支付宝、天猫、淘宝、优酷视频、喜马拉雅、网易云音乐、哔哩哔哩等APP的Banner样式以及指示器样式。

欢迎大家到github关注BannerViewPager