Activity界面路由的一种简单实现

1. 引言

平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Activity页面时。这种情况下,网页的调用代码可能是app.openPage("/testPage")这样,或者是用app.openPage("local://myapp.com/loginPage")这样的方式,我们需要用一种方式把路径和页面关联起来。Android可以允许我们在Manifest文件中配置标签来达到类似效果,也可以使用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 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属性时注意添加对应的混淆设置,避免因混淆而导致找不到对应方法。

你可能感兴趣的:(Java,android,java,router,activity)