平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent
,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Activity页面时。这种情况下,网页的调用代码可能是app.openPage("/testPage")
这样,或者是用app.openPage("local://myapp.com/loginPage")
这样的方式,我们需要用一种方式把路径和页面关联起来。Android可以允许我们在Manifest文件中配置标签来达到类似效果,也可以使用
ARouter
框架来实现这样的功能。本文就用200行左右的代码实现一个类似ARouter
的简易界面路由。
这个操作建议放在Application的onCreate
方法中,在第一次调用Router来打开页面之前。
public class AppContext extends Application {
@Override
public void onCreate() {
super.onCreate();
Router.init(this);
}
}
这是最简单的情况,只需要提供一个路径,适合“关于我们”、“隐私协议”这种简单无参数页面。
Activity配置:
@Router.Path("/testPage")
public class TestActivity extends Activity {
//......
}
启动代码:
Router.from(mActivity).toPath("/testPage").start();
//或
Router.from(mActivity).to("local://my.app/testPage").start();
这是比较常见的情况,需要在注解中声明需要的参数名称,这些参数都是必要参数,如果启动的时候没有提供对应参数,则发出异常。
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();
有一些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();
这个注解只能用于Activity的子类,表示这个Activity需要页面路由的功能。这个类有三个属性:
value
:表示这个Activity的相对路径。args
:表示这个Activity需要的参数,都是必要参数,如果打开页面时缺少指定参数,就会发出异常。method
:如果这个Activity需要静态方法做为入口,就将这个属性指定为方法名,并给对应方法添加Entry
注解。(注意:这个属性值不为空时,忽略这个注解中的args
属性内容)这个注解只能用于Activity的静态方法,表示这个方法作为打开Activity的入口。仅包含一个属性:
args
:表示这个方法需要的参数。public static void init(Context context)
onCreate
方法中完成初始化。public static Router from(Activity activity)
public RouterBuilder to(String urlString)
public RouterBuilder toPath(String path)
to
需要执行绝对路径,而toPath
需要指定相对路径。返回的RouterBuilder
用于接收打开页面需要的参数。
public RouterBuilder with(String key, String value)
public RouterBuilder with(String key, int value)
Bundle
的各个put
方法。目前只有常用的String
和int
两个类型。如有需要可自行在RouterBuilder
中添加对应的方法。public void start()
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 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 {};
}
}
ARouter
类似,实际项目中建议使用ARouter
。如果有特殊需求,例如,页面参数的检查或定制具体打开行为,可以考虑基于这个工具进行修改。Path
的method
属性时注意添加对应的混淆设置,避免因混淆而导致找不到对应方法。