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 属性时,需要用Entry 的args 来声明参数。
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的入口。仅包含一个属性:
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方法
public RouterBuilder to(String urlString) public RouterBuilder toPath(String path)
- 方法说明:这个方法用于指定目标的路径,
to 需要执行绝对路径,而toPath 需要指定相对路径。返回的RouterBuilder 用于接收打开页面需要的参数。
3.4 RouterBuilder.with方法
public RouterBuilder with(String key, String value) public RouterBuilder with(String key, int value)
- 方法说明:这个方法用于添加参数,对应
Bundle 的各个put 方法。目前只有常用的String 和int 两个类型。如有需要可自行在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. 注意
- 这个工具的一些功能与
ARouter 类似,实际项目中建议使用ARouter 。如果有特殊需求,例如,页面参数的检查或定制具体打开行为,可以考虑基于这个工具进行修改。 - 使用了
Path 的method 属性时注意添加对应的混淆设置,避免因混淆而导致找不到对应方法。
|