从0开始,编写一个组件化开发框架<一>

本文主要内容:
组件之间如何进行通信?暂不涉及组件单独编译的动态配置。


一、新建一个项目

app模块之外,再新建四个模块,分别为usercenter(用户中心),order(订单),payment(支付)和common(统一工具);由于本框架考虑使用注解+APT技术,所以再新建两个java模块,分别为router(注解),router_compiler(注解处理器)。项目结构大致如下:

项目目录结构

二、模块分工

app: 主模块,负责展示商品;
common:基础工具层,提供网络、统一封装;
usercenter:用户中心,用户信息操作相关;
order:订单模块,用户订单相关操作;
payment:支付模块,用户支付订单相关操作;
router:定义组件化相关注解;
router_compiler:注解处理器模块;
app的主要流程:用户打开app,进入到app模块,进行商品的选购;选购完成之后,下单进入order模块,order模块操作完成之后,进入支付模块进行付款;付款完成之后返回订单模块。
app模块可以进入到个人中心,个人中心可以查看订单;

三、开始代码编写

组件与组件之间是相互没有依赖的,那么,我们组件之间如何进行通信呢?
一般有两种思路:

  1. 像EventBus那样,做一个事件总线,使用发布-订阅的模式,各组件向总线订阅消息,有消息时进行处理。
  2. 做一个路由框架,用一个路由表来一一对应各组件对外公开的业务,其他组件可以通过这个路由表与相应的组件进行通信。

这里我们选择第二种做法。
一种比较简单的实现就是在common模块,手动维护一个路由表,每个模块有对外的业务就要这个路由表里增加一条路由。当然,这是可以实现组件之间的通信的,不过每个模块都去手动修改同一个路由表,比较麻烦,也容易造成冲突,不易维护。
这里我们使用注解+注解处理器的方式,自动的生成路由,可以避免这种情况。

首先,定义注解

router模块里,定义一个注解:

@Target(ElementType.TYPE) //只作用于类
@Retention(RetentionPolicy.CLASS) //编译期保留
public @interface Router {
    /**
     * 获取路径
     *
     * @return 路径,做为路由表的key
     */
    String path();
}

需要提供服务的页面就加上这个注解,如payment模块需要对外提供支付服务,那么我们在payment模块里的PaymentActivity上加上Router注解,并传入一个path:

/**
 * 支付组件首页,展示支付方式,调起具体支付流程,并返回给调用方支付结果
 */
@Router(path = "com.example.payment.PaymentActivity")
public class PaymentActivity extends BaseActivity {
    //...
}

这里的path我们直接使用类的全类名。

那么,Router注解标记的类,我们应该怎么处理呢?
我们应该拿到注解标记的类,对应的path,生成一个把这些信息添加进路由表,注解处理器就可以做到这个工作。

注解处理器,处理注解

@AutoService(Processor.class) //注册
@SupportedAnnotationTypes(ProcessorConfig.ROUTER_PACKAGE) //指明需要处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions({ProcessorConfig.OPTIONS, ProcessorConfig.APT_PACKAGE})
public class RouterProcessor extends AbstractProcessor {

    // 操作Element的工具类(类,函数,属性,其实都是Element)
    private Elements elementTool;

    // type(类信息)的工具类,包含用于操作TypeMirror的工具方法
    private Types typeTool;

    // Message用来打印 日志相关信息
    private Messager messager;

    // 文件生成器, 类 资源 等,就是最终要生成的文件 是需要Filer来完成的
    private Filer filer;
    //
    private String options;  // (模块传递过来的)模块名  app,personal
    private String aptPackage; // (模块传递过来的) 包名

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        this.elementTool = processingEnvironment.getElementUtils();
        this.typeTool = processingEnvironment.getTypeUtils();
        this.filer = processingEnvironment.getFiler();
        this.messager = processingEnvironment.getMessager();
        print("RouterProcessor init");
        // 只有接受到 App壳 传递过来的书籍,才能证明我们的 APT环境搭建完成
        options = processingEnvironment.getOptions().get(ProcessorConfig.OPTIONS);
        aptPackage = processingEnvironment.getOptions().get(ProcessorConfig.APT_PACKAGE);
        print("options = " + options);
        print("aptPackage = " + aptPackage);
        if (options != null && aptPackage != null) {
            print("APT环境搭建完成");
        } else {
            print("APT环境有问题,请检查options 和 aptPackage");
        }
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 获取到系统类Activity的类型
        TypeElement activityType = elementTool.getTypeElement(ProcessorConfig.ACTIVITY_PACKAGE);
        TypeMirror activityMirror = activityType.asType();

        Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(Router.class);
        if (elementSet != null) {
            for (Element element : elementSet) {
                print("发现@Router注解的地方" + element.getSimpleName());

//                Router router = element.getAnnotation(Router.class);
                // 必须是Activity
                TypeMirror elementMirror = element.asType(); // 当前 == Activity
                if (typeTool.isSubtype(elementMirror, activityMirror)) {
                    print("检查当前被注解的类,确认是Activity类或及子类");
                } else {
                    // 不匹配抛出异常,这里谨慎使用!考虑维护问题
                    throw new RuntimeException("@ARouter注解目前仅限用于Activity类之上");
                }
            }

            //获取到使用注解的类
            //通过得到的信息,自动生成一个路由表
            TypeElement implementType = elementTool.getTypeElement(ProcessorConfig.IROUTER_PATH);
            try {
                createPathFile(elementSet, implementType);
            } catch (IOException e) {
                e.printStackTrace();
                print("自动创建路由表失败");
            }
            return true;
        }
        return false;
    }

    /**
     * PATH 生成
     *
     * @param implementType
     * @throws IOException
     */
    private void createPathFile(Set<? extends Element> elementSet, TypeElement implementType) throws IOException {
        // 判断 map仓库中,是否有需要生成的文件
        if (elementSet.isEmpty()) {
            return;
        }

        //类属性 private Map<String, Class> mRouterMap = new HashMap<>();
        TypeName mapTypeName = ParameterizedTypeName.get(
                ClassName.get(Map.class), // Map
                ClassName.get(String.class), // Map<String,
                ClassName.get(Class.class)); // Map<String, Class>

        MethodSpec.Builder onCreateMethodBuilder = MethodSpec.methodBuilder(ProcessorConfig.METHOD_ONCREATE)
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .returns(mapTypeName);

        //方法体 mRouterMap.put(string, clazz.class);
        for (Element element : elementSet) {
            Router router = element.getAnnotation(Router.class);
            onCreateMethodBuilder.addStatement("$T<$T, $T> routerMap = new $T<>()",
                    ClassName.get(Map.class), // Map
                    ClassName.get(String.class), // String
                    ClassName.get(Class.class), // Class
                    ClassName.get(HashMap.class)); // HashMap
            onCreateMethodBuilder.addStatement("routerMap.put($S, $T.class)",
                    router.path(),
                    ClassName.get(((TypeElement) element)))
                    .addStatement("return routerMap")
                    .returns(mapTypeName);
        }

        //类定义 public class RouterManager implements IRouter {...}
        String clazzName = ProcessorConfig.ROUTER_MANAGER + "$" + options;
        TypeSpec className = TypeSpec.classBuilder(clazzName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(onCreateMethodBuilder.build())
                .addSuperinterface(ClassName.get(implementType))
                .build();

        // 生成 和 类 等等,结合一体
        JavaFile.builder(aptPackage, // 包名
                className) // 类构建完成
                .build() // JavaFile构建完成
                .writeTo(filer); // 文件生成器开始生成类文件

        print("自动生成" + clazzName + "成功,请在build/generated/ap_generated_sources/debug/out" + "目录下查看");
    }


    private void print(String msg) {
        messager.printMessage(Diagnostic.Kind.NOTE, msg);
    }

如上,我们在router_compiler模块里新建一个处理器类RouterProcessor 继承自AbstractProcessor,并实现其两个方法,
init(): 做一些初始化操作,如获取工具类,Messager(打印日志),filer(文件生成器)等等
process(): 对注解进行处理。

这里需要说明一下RouterProcessor类使用到的注解:
@AutoService(Processor.class) : 把此注解处理类,进行注册。不注册不能发挥作用。
@SupportedAnnotationTypes(ProcessorConfig.ROUTER_PACKAGE) :指明需要处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_7) :指明Java版本

process()方法里,我们拿到Router注解处理的类集合,根据这个集合,调用createPathFile()生成我们的路由表。

createPathFile()生成路由表的过程,可以通过硬编码的形式写文件,不过这种方法容易出错。这里我们使用javapoet来生成代码,如何使用请移步https://github.com/square/javapoet
下面是自动生成的payment模块的路由表。

public class RouterTable$payment implements IRouter {
  @Override
  public Map<String, Class> onCreate() {
    Map<String, Class> routerMap = new HashMap<>();
    routerMap.put("com/example/payment/PaymentActivity", PaymentActivity.class);
    return routerMap;
  }
}
payment模块路由表目录

说明:IRouter是在模块router里定义的一个接口,用于给各模块的路由表制定一个标准。里面定义一个方法,返回一个Map<String,Class>对象。

public interface IRouter {
    Map<String, Class> onCreate();
}

到此,我们已经完成一大部分了,第个模块都生成了自己的路由表。但是这些路由表都是在各自的模块中,依然无法被其他模块引用到。此时我们想到,是否可以在common模块里,定义一个RouterManager处理类,把各模块的路由表集中到这个类里进行管理呢?

public enum RouterManager {
    instance;

    private Map<String, Class> mRouterMap;

    public void addRouterTable(Map<String, Class> routerTable) {
        if (mRouterMap == null) {
            mRouterMap = new HashMap<>();
        }
        mRouterMap.putAll(routerTable);
    }

    public void startActivityWith(Context context, String path, Bundle bundle) {
        if (path == null || path.isEmpty()) {
            throw new RuntimeException("path can not be null or empty!");
        }

        Class clazz = mRouterMap.get(path);
        if (clazz != null) {
            Intent intent = new Intent(context, clazz);
            if (bundle != null) {
                intent.putExtras(bundle);
            }
            context.startActivity(intent);
        } else {
            ToastManager.show(context, "要启动的Activity未注册: " + path);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public void startActivityForResultWith(Context context, String path, Bundle bundle, int requestCode) {
        if (context instanceof Activity) {
            Class clazz = mRouterMap.get(path);
            if (clazz != null) {
                Intent intent = new Intent(context, clazz);
                if (bundle != null) {
                    intent.putExtras(bundle);
                }
                ((Activity) context).startActivityForResult(intent, requestCode);
            } else {
                ToastManager.show(context, "要启动的Activity未注册: " + path);
            }
        }
    }
}

可以看到,这里是使用枚举定义了一个单例,提供了三个方法:
addRouterTable():往mRouterMap里添加路由表;
startActivityWith()startActivityForResultWith()则是根据根据传入的path,对路由进行查找,匹配之后,进行跨组件跳转。

上在只是提供了添加路由的方法,但是在什么时候执行呢?
当然是在app启动越早越好,因为你并不知道用户从启动app到需要跨组件需要多久,也许很快。
所以我们想到,自定义一个Application,在Application的onCreate方法里,开个异步线程,通过反射获取实例化对应模块的路由表,然后调用RouterManager.instance.addRouterTable(routerMap);把路由添加进行RouterManager

public class BaseApplication extends Application {

    private static final String TAG = "BaseApplication >>>>> ";

    @Override
    public void onCreate() {
        super.onCreate();

        new Thread(new Runnable() {
            @Override
            public void run() {
                String[] bundles = new String[]{
                        "com.example.router.RouterTable$payment",
                        "com.example.router.RouterTable$order",
                        "com.example.router.RouterTable$usercenter"
                };
                for (String bundle : bundles) {
                    try {
                        Class clazz = Class.forName(bundle);
                        IRouter iRouter = (IRouter) clazz.newInstance();
                        Map<String, Class> routerMap = iRouter.onCreate();
                        RouterManager.instance.addRouterTable(routerMap);
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    }
                }
                Log.i(TAG, "路由表已建好...");
            }
        }).start();
    }
}

执行跨组件通信

order模块的OrderActivity里,通过RouterManager.instance.startActivityForResultWith(this, "com/example/payment/PaymentActivity", bundle, payRequestCode);调用到支付模块进行支付:

@Router(path = "com.example.order.OrderActivity")
public class OrderActivity extends BaseActivity {
    private static final int payRequestCode = 111;
    private Button btnPay;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_order);

        initView();
    }

    @Override
    public void initView() {
        btnPay = findViewById(R.id.btnPay);
        btnPay.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        super.onClick(v);
        if (v.getId() == R.id.btnPay) {
            Bundle bundle = new Bundle();
            bundle.putString("orderSn", "123456789");
            bundle.putFloat("orderAmount", 99.99f);
            RouterManager.instance.startActivityForResultWith(this, "com.example.payment.PaymentActivity", bundle, payRequestCode);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == payRequestCode) {
            if (resultCode != Activity.RESULT_OK) {
                return;
            }

            boolean paySuccess = data != null && data.getBooleanExtra("payResult", false);
            toast("订单'123456789'支付" + (paySuccess ? "成功" : "失败"));
        }
    }
}

到此,我们的组件化后,组件(Activity与Activity)之间的通信就完成了。

你可能会问了,如果与另一个组件的非Activity类进行通信,怎么做呢?其实原理都是一样的,这里就不贴代码了。

最后,再附上payment模块的gradle配置

apply plugin: 'com.android.library'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"


    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'

        // 都是为了 传递给 注解处理器
        // 在gradle文件中配置选项参数值(用于APT传参接收)
        // 切记:必须写在defaultConfig节点下
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName(), packageNameForAPT: packageNameForAPT] //packageNameForAPT只是一个字符串,一个包名,存入模块生成的路由类。
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    // 公共基础库
    implementation project(path: ":common")

    // router 专用 注解模块
    implementation project(path: ":router")

    // router 专用 注解处理器
    annotationProcessor project(path: ':router_compiler')
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容