1. 引言

平常Android开发中,发动Activity对错常常见的操作,而翻开一个新Activity能够直接运用Intent,也能够每个Activity供给一个静态的发动办法。可是有些时分运用这些办法并不那么方便,比如:一个应用内的网页需求翻开一个原生Activity页面时。这种情况下,网页的调用代码可能是app.openPage("/testPage")这样,或者是用app.openPage("local://myapp.com/loginPage")这样的办法,咱们需求用一种办法把途径和页面相关起来。Android能够答应咱们在Manifest文件中装备<data>标签来到达相似作用,也能够运用ARouter结构来完结这样的功能。本文就用200行左右的代码完结一个相似ARouter的简易界面路由

2. 示例

2.1 初始化

这个操作主张放在Application的onCreate办法中,在第一次调用Router来翻开页面之前。

public class AppContext extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Router.init(this);
    }
}

2.2 发动无参数Activity

这是最简略的情况,只需求供给一个途径,适合“关于咱们”、“隐私协议”这种简略无参数页面。

Activity装备:

@Router.Path("/testPage")
public class TestActivity extends Activity {
    //......
}

发动代码:

Router.from(mActivity).toPath("/testPage").start();
//或
Router.from(mActivity).to("local://my.app/testPage").start();

2.3 发动带参数Activity

这是比较常见的情况,需求在注解中声明需求的参数称号,这些参数都是必要参数,假如发动的时分没有供给对应参数,则宣布异常。

Activity装备:

@Router.Path(value = "/testPage",args = {"id", "type"})
public class TestActivity extends Activity {
        @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //加载布局...
        String id = getIntent().getStringExtra("id"); //获取参数
        int type = getIntent().getIntExtra("type", 0);//获取参数
    }
}

发动代码:

Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

2.4 发动带有静态发动办法的Activity

有一些Activity需求通过它供给的静态办法发动,就能够运用Path中的method特点和Entry注解来声明进口,能够供给参数。在供给了method特点时,需求用Entryargs来声明参数。

Activity装备:

@Router.Path(value = "/testPage", method = "open")
public class TestActivity extends Activity {
    @Router.Entry(args = {"id", "type"})
    public static void open(Activity activity, Bundle args) {
        Intent intent = new Intent(activity, NestWebActivity.class);
        intent.putExtras(args);
        activity.startActivity(intent);
    }
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //加载布局...
        String id = getIntent().getStringExtra("id"); //获取参数
        int type = getIntent().getIntExtra("type", 0);//获取参数
    }
}

发动代码:

Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

3. API介绍

3.1 Path注解

这个注解只能用于Activity的子类,表明这个Activity需求页面路由的功能。这个类有三个特点:

  • value:表明这个Activity的相对途径。
  • args:表明这个Activity需求的参数,都是必要参数,假如翻开页面时缺少指定参数,就会宣布异常。
  • method:假如这个Activity需求静态办法做为进口,就将这个特点指定为办法名,并给对应办法增加Entry注解。(留意:这个特点值不为空时,忽略这个注解中的args特点内容)

3.1 Entry注解

这个注解只能用于Activity的静态办法,表明这个办法作为翻开Activity的进口。仅包括一个特点:

  • args:表明这个办法需求的参数。

3.2 Router.init办法

  • 办法签名:public static void init(Context context)
  • 办法阐明:这个办法用于初始化页面路由表,必须在第一次用Router翻开页面之前完结初始化。主张在Application的onCreate办法中完结初始化。

3.3 Rouater.from办法

  • 办法签名:public static Router from(Activity activity)
  • 办法阐明:这个办法用于创建Router实例,传入的参数一般为当前Activity。例如,要从AActivity翻开BActivity,那么传入参数为AActivity的实例。

3.4 Rouater.to和Rouater.toPath办法

  • 办法签名:
  1. public RouterBuilder to(String urlString)
  2. public RouterBuilder toPath(String path)
  • 办法阐明:这个办法用于指定方针的途径,to需求执行绝对途径,而toPath需求指定相对途径。回来的RouterBuilder用于接收翻开页面需求的参数。

3.4 RouterBuilder.with办法

  • 办法签名:
  1. public RouterBuilder with(String key, String value)
  2. public RouterBuilder with(String key, int value)
  • 办法阐明:这个办法用于增加参数,对应Bundle的各个put办法。现在只要常用的Stringint两个类型。如有需求可自行在RouterBuilder中增加对应的办法。

3.4 RouterBuilder.start办法

  • 办法签名:public void start()
  • 办法阐明:这个办法用于翻开页面。假如存在途径错误、参数错误等异常情况,会宣布对应运行时异常。

4. 完结

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.Keep;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@Keep
public class Router {
    public static final String SCHEME = "local";
    public static final String HOST = "my.app";
    public static final String URL_PREFIX = SCHEME + "://" + HOST;
    private static final Map<String, ActivityStarter> activityPathMap = new ConcurrentHashMap<>();
    public static void init(Context context) {
        try {
            PackageManager packageManager = context.getPackageManager();
            PackageInfo packageInfo = packageManager.getPackageInfo(
                    context.getPackageName(), PackageManager.GET_ACTIVITIES);
            for (ActivityInfo activityInfo : packageInfo.activities) {
                Class<?> aClass = Class.forName(activityInfo.name);
                Path annotation = aClass.getAnnotation(Path.class);
                if (annotation != null && !TextUtils.isEmpty(annotation.value())) {
                    activityPathMap.put(annotation.value(), (Activity activity, Bundle bundle) -> {
                        if (TextUtils.isEmpty(annotation.method())) {
                            for (String arg : annotation.args()) {
                                if (!bundle.containsKey(arg)) {
                                    throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
                                }
                            }
                            Intent intent = new Intent(activity, aClass);
                            intent.putExtras(bundle);
                            activity.startActivity(intent);
                        } else {
                            try {
                                Method method = aClass.getMethod(annotation.method(), Activity.class, Bundle.class);
                                Entry entry = method.getAnnotation(Entry.class);
                                if (entry != null) {
                                    for (String arg : entry.args()) {
                                        if (!bundle.containsKey(arg)) {
                                            throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
                                        }
                                    }
                                    method.invoke(null, activity, bundle);
                                } else {
                                    throw new IllegalStateException("can not find a method with [Entry] annotation!");
                                }
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        }
                    });
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    public static Router from(Activity activity) {
        return new Router(activity);
    }
    private final Activity activity;
    private Router(Activity activity) {
        this.activity = activity;
    }
    public RouterBuilder to(String urlString) {
        if (TextUtils.isEmpty(urlString)) {
            return new ErrorRouter(new IllegalArgumentException("argument [urlString] must not be null"));
        } else {
            return to(Uri.parse(urlString));
        }
    }
    public RouterBuilder toPath(String path) {
        return to(Uri.parse(URL_PREFIX + path));
    }
    public RouterBuilder to(Uri uri) {
        try {
            if (SCHEME.equals(uri.getScheme())) {
                if (HOST.equals(uri.getHost())) {
                    String path = uri.getPath();//note: 二级途径暂不考虑
                    ActivityStarter starter = activityPathMap.get(path);
                    if (starter == null) {
                        throw new IllegalStateException(String.format("path [%s] is not support", path));
                    } else {
                        NormalRouter router = new NormalRouter(activity, starter);
                        for (String key : uri.getQueryParameterNames()) {
                            if (!TextUtils.isEmpty(key)) {
                                router.with(key, uri.getQueryParameter(key));
                            }
                        }
                        return router;
                    }
                } else {
                    throw new IllegalArgumentException(String.format("invalid host : %s", uri.getHost()));
                }
            } else {
                throw new IllegalArgumentException(String.format("invalid scheme : %s", uri.getScheme()));
            }
        } catch (RuntimeException e) {
            return new ErrorRouter(e);
        }
    }
    public static abstract class RouterBuilder {
        public abstract RouterBuilder with(String key, String value);
        public abstract RouterBuilder with(String key, int value);
        public abstract void start();
    }
    private static class ErrorRouter extends RouterBuilder {
        private final RuntimeException exception;
        private ErrorRouter(RuntimeException exception) {
            this.exception = exception;
        }
        @Override
        public RouterBuilder with(String key, String value) {
            return this;
        }
        @Override
        public RouterBuilder with(String key, int value) {
            return this;
        }
        @Override
        public void start() {
            throw exception;
        }
    }
    private static class NormalRouter extends RouterBuilder {
        final Activity activity;
        final Bundle bundle = new Bundle();
        final ActivityStarter starter;
        private NormalRouter(Activity activity, ActivityStarter starter) {
            this.activity = Objects.requireNonNull(activity);
            this.starter = Objects.requireNonNull(starter);
        }
        @Override
        public RouterBuilder with(String key, String value) {
            bundle.putString(key, value);
            return this;
        }
        @Override
        public RouterBuilder with(String key, int value) {
            bundle.putInt(key, value);
            return this;
        }
        @Override
        public void start() {
            starter.start(activity, bundle);
        }
    }
    @FunctionalInterface
    private interface ActivityStarter {
        void start(Activity activity, Bundle bundle);
    }
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Path {
        String value();
        String method() default "";
        String[] args() default {};
    }
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Entry {
        String[] args() default {};
    }
}

5. 留意

  1. 这个东西的一些功能与ARouter相似,实际项目中主张运用ARouter。假如有特殊需求,例如,页面参数的查看或定制详细翻开行为,能够考虑基于这个东西进行修正。
  2. 运用了Pathmethod特点时留意增加对应的混淆设置,避免因混淆而导致找不到对应办法。