介绍

在组件化开发的时候,组件之间是相互独立的没有依赖关系,我们不能在使用显示调用来跳转页面了,因为我们组件化的目的之一就是解决模块间的强依赖问题,假如现在要从A业务组件跳转到业务B组件,并且要携带参数跳转,这时候怎么办呢?

上学的时候在书上看到了一句很有意义的话:任何软件工程遇到的问题都可以通过增加一个中间层来解决!

我们从这句话出发去思考:组件之间是平行结构的,它们之间相互没有交集,要实现通信只有添加一个中间层将它们连接到一起,这就是“路由”的概念,由第一篇文章开始的组件化模型下的业务关系图可知路由就是起到一个转发的作用。路由就像一个桥梁一样让平行的河流(组件)之间可以通信和传递数据,这样看起来好像他们之间又是强耦合的关系,维护起来还是代价还有点大,那么还有没有好点的办法呢?那就是路由层使用接口和其他module之间建立弱耦合的关系。

在组件化开发中实现跨组件跳转的库比较有名气的是阿里的ARouter,ARouter的功能很丰富,这一节我们根据ARouter的源码理解自己实现一个简单版的ARouter,ARouter的功能太多了如过滤器,拦截器等,这里只是手写实现一部分核心功能-路由跳转。

准备

实现

定义注解

在router_annotation的module中定义注解Route.java

/**

* Target用于指定被此元注解标注的注解可以标出的程序元素

*

*/

@Target(ElementType.TYPE)

/**

* RetentionPolicy.SOURCE 源码阶段 注解信息只会保留在源码中,编译器在编译源码的时候会将其直接丢弃

* RetentionPolicy.CLASS 编译阶段 javapoet使用 注解信息保留在class文件中,VM不会持有其信息

* RetentionPolicy.RUNTIME 运行阶段,注解信息保留在class文件中,而且VM也会持有此注解信息, 反射获得注解信息

*/

@Retention(RetentionPolicy.CLASS)

public @interface Route {

/**

* 路由的路径,标识一个路由节点

*/

String path();

/**

* 将路由节点进行分组,可以实现按组动态加载

*/

String group() default "";

}

Route注解里有path和group,这是仿照ARouter对路由进行分组。因为当项目变得越来越庞大的时候,为了便于管理和减小首次加载路由表过于耗时的问题,我们对所有的路由进行分组。

定义注解处理器生成路由映射文件

定义RouteProcessor实现AbstractProcessor类, 这里使用google的 AutoService注册处理器,使用JavaPoet实现源文件的编写。在router_compiler的build.gradle引入这两个库

apply plugin: 'java-library'

dependencies {

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

annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'

compileOnly 'com.google.auto.service:auto-service:1.0-rc4'

implementation 'com.squareup:javapoet:1.11.1'

implementation project(':router_annotation')

}

// java控制台输出中文乱码

tasks.withType(JavaCompile) {

options.encoding = "UTF-8"

}

sourceCompatibility = "7"

targetCompatibility = "7"

Processor 一般会重写父类的4个方法:

init:初始化工作,我们可以得到一些有用的工具,例如 Filer,ElementUtils,TypeUtils.

process:最重要的方法,所有的注解处理都是在此完成

getSupportedAnnotationTypes:返回我们所要处理的注解的一个集合

getSupportedSourceVersion:要支持的java版本

@AutoService(Processor.class)

//处理器接受到的参数 代替 {@link AbstractProcessor#getSupportedOptions()} 函数

@SupportedOptions(Constants.ARGUMENTS_NAME)

/**

* 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函数

* 声明我们注解支持的JDK的版本

*/

@SupportedSourceVersion(SourceVersion.RELEASE_7)

/**

* 注册给哪些注解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数

* 声明我们要处理哪一些注解 该方法返回字符串的集合表示该处理器用于处理哪些注解

*/

@SupportedAnnotationTypes(Constants.ANN_TYPE_ROUTE)

public class RouteProcessor extends AbstractProcessor {

....

/**

* key是组名,value是注解标记的element的元素数据集合

* 使用Route的注解按照组名分表存储

*/

private Map> groupMap = new HashMap<>();

/**

* key:组名 value:实现IRouteGroup接口的className

*/

private Map rootMap = new TreeMap<>();

...

}

init方法中根据ProcessingEnvironment得到一些工具和参数

@Override

public synchronized void init(ProcessingEnvironment processingEnvironment) {

super.init(processingEnvironment);

Messager ms = processingEnvironment.getMessager();

Log.init(ms);

elementUtils = processingEnvironment.getElementUtils();

filerUtils = processingEnvironment.getFiler();

typeUtils = processingEnvironment.getTypeUtils();

Map options = processingEnvironment.getOptions();

if (!Utils.isEmpty(options)) {

moduleName = options.get(Constants.ARGUMENTS_NAME);

}

if (Utils.isBlank(moduleName)) {

throw new RuntimeException("Not set Processor Parmaters.");

}

}

扫描所有Route修饰的注解文件,根据组名进行分组

/**

* 处理注解

*

* @param set 使用了支持注解的节点集合

* @param roundEnvironment 上下文

* @return true 表示后续处理器不会再处理

*/

@Override

public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {

if (!Utils.isEmpty(set)) {

Set extends Element> elementsAnnotateds = roundEnvironment.getElementsAnnotatedWith(Route.class);

if (!Utils.isEmpty(elementsAnnotateds)) {

try {

parseRoute(elementsAnnotateds);

} catch (Exception e) {

Log.i("创建实现类异常---》"+e);

}

}

}

return true;

}

private void parseRoute(Set extends Element> routeElements) throws Exception {

// 通过elementUtils获得节点

TypeElement activityTypeElement = elementUtils.getTypeElement(Constants.ACTIVITY);

//mirror 节点自描述

TypeMirror activityTypeMirror = activityTypeElement.asType();

TypeElement IServiceTypeElement = elementUtils.getTypeElement(Constants.ISERVICE);

TypeMirror IServiceTypeMirror = IServiceTypeElement.asType();

Log.i("activityTypeMirror=" + activityTypeMirror + ", IServiceTypeMirror=" + IServiceTypeMirror);

RouteMeta routeMeta;

for (Element routeElement : routeElements) {

Log.i("routeElement="+routeElement);

Route route = routeElement.getAnnotation(Route.class);

TypeMirror typeMirror = routeElement.asType();

if (typeUtils.isSubtype(typeMirror, activityTypeMirror)) {//activity节点

routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, routeElement);

}/* else if (typeUtils.isSubtype(typeMirror, IServiceTypeMirror)) {//IService节点

routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, routeElement);

} **/else {

throw new RuntimeException("[Just Support Activity/IService Route] :" + routeElement);

}

//分组记录信息, groupMap

categories(routeMeta);

}

//生成类 需要实现的接口

generatedGroup();

generatedRoot();

}

每次检索到一个注解元素routeElement都要去groupMap 分类。

private void categories(RouteMeta routeMeta) throws ClassNotFoundException {

if (routeVerify(routeMeta)) {//1

Log.i("group name =" + routeMeta.getGroup() + ", path=" + routeMeta.getPath());

List routeMetas = groupMap.get(routeMeta.getGroup());

if (Utils.isEmpty(routeMetas)) {

routeMetas = new ArrayList<>();

groupMap.put(routeMeta.getGroup(), routeMetas);

}

//2

/* Class> destination = routeMeta.getDestination();

if (destination == null) {

String className = routeMeta.getElement().asType().toString();

Log.i("name="+className);

destination = Class.forName(className);

routeMeta.setDestination(destination);

}*/

routeMetas.add(routeMeta);

} else Log.e("group info error:" + routeMeta.getPath());

}

routeMeta合法后开始根据groupName进行分组归类。

上边的注释2处,我的本意是通过反射给routeMeta.setDestination设置值,在后边生成文件的时候直接通过routeMeta.getsetDestination使用。但是这样做的时候会报错 ClassNotFoundException。一时半会也不知道原因,晚上在看书的时候突然想起来,这是在编译期完成的,反射是在运行期使用,所以会报ClassNotFoundException。

注释1处的是验证routeMeta是否包含group,包含group条件是,注解的时候声明group,或者path的路径必须是// 大于等于两个“/”

private boolean routeVerify(RouteMeta meta) {

String path = meta.getPath();

String group = meta.getGroup();

//路由地址必须是/ 开头

if (Utils.isBlank(path) || !path.startsWith("/")) {

return false;

}

String[] split = path.split("/");

if (split.length < 3) {

return false;

}

//如果没有分组就以第一个/后的节点为分组

if (Utils.isBlank(group)) {

String defaultGroup = split[1];

if (Utils.isBlank(defaultGroup)) {

return false;

}

meta.setGroup(defaultGroup);

}

return true;

}

对RouteMeta进行分组整理后,在parseRoute方法中根据组名生成IRouteGroup的实现类,实现类中接受map参数,将扫描的路由文件存储到map中。IRouteGroup接口的定义如下:

public interface IRouteGroup {

/**

* 扫描注解 搜集路由信息

* @param atlas key:路由路径 path,value:Route注解修饰的路由信息数据

*/

void loadInto(Map atlas);

}

该文件是在route_core的module中声明的。

下边是通过Apt实现该接口的方法。

/**

* 生成IRouteGroup的实现类

* @throws Exception

*/

private void generatedGroup() throws Exception {

//参数类型 Map

ParameterizedTypeName atlas = ParameterizedTypeName.get(

ClassName.get(Map.class),

ClassName.get(String.class),

ClassName.get(RouteMeta.class)

);

//创建参数 Map atlas

ParameterSpec groupParamSpec = ParameterSpec.builder(atlas, "atlas").build();

//遍历分组,每一个分组创建一个类

for (Map.Entry> entry : groupMap.entrySet()) {

//声明方法

// @Override

// public void loadTo(Map atlas)

MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(Constants.METHOD_LOAD_INTO)

.addModifiers(Modifier.PUBLIC)

.addAnnotation(Override.class)

.returns(void.class)

.addParameter(groupParamSpec);

//方法体 实现接口的抽象方法,将分组后的路由信息插入atlas中

//key 是path,value---RouteMeta

String groupName = entry.getKey();

List routeMetas = entry.getValue();

for (RouteMeta routeMeta : routeMetas) {

//atlas.put(path, RouteMeta.build(class, path, group)

// RouteMeta build(Type type, Class> destination, String path, String group)

loadIntoMethodOfGroupBuilder.addStatement("atlas.put($S, $T.build($T.$L,$T.class, $S, $S))",

routeMeta.getPath(),

ClassName.get(RouteMeta.class),

ClassName.get(RouteMeta.Type.class),

routeMeta.getType(),

ClassName.get((TypeElement) routeMeta.getElement()),

routeMeta.getPath().toLowerCase(),

groupName.toLowerCase()

);

}

TypeElement iRouteGroupTypeElement = elementUtils.getTypeElement(Constants.ROUTE_GROUP);

//创建Java文件

String groupClassName = Constants.NAME_OF_GROUP + groupName;

Log.i("groupClassName="+groupClassName);

JavaFile.builder(Constants.PACKAGE_OF_GENERATE_FILE,

TypeSpec.classBuilder(groupClassName)

.addModifiers(Modifier.PUBLIC)

.addSuperinterface(ClassName.get(iRouteGroupTypeElement))

.addMethod(loadIntoMethodOfGroupBuilder.build()).build()

).build().writeTo(filerUtils);

Log.i("Generated RouteGroup: " + Constants.PACKAGE_OF_GENERATE_FILE + "." +

groupClassName);

rootMap.put(groupName, groupClassName);

}

}

在编写的时候这里需要格外小心,出错了很难排查,因为这是编译期不能进行debug,所以只能借助Log在关键的地方加上日志。另外在编写的时候,可以自己先在module2中创建一个实现该接口的类Router$$Group$$test,然后对比着进行编码,最后记得把Router$$Group$$test注释掉,

public class Router$$Group$$test implements IRouteGroup {

private Map> groupMap;

@Override

public void loadInto(Map atlas) {

for (String groupName : groupMap.keySet()) {

List routeMetas = groupMap.get(groupName);

for (RouteMeta routeMeta : routeMetas) {

//RouteMeta build(Type type, Class> destination, String path, String group)

Class> destination = routeMeta.getDestination();

if (destination == null) {

destination = ClassName.get((Type) routeMeta.getElement());

}//

atlas.put(routeMeta.getPath(),

RouteMeta.build(routeMeta.getType(),

routeMeta.getDestination(),

routeMeta.getPath(),

routeMeta.getGroup()));

}

}

}

}

回到parseRoute中下一步是generatedRoot()

private void generatedRoot() throws Exception {

TypeElement iRouteRootTypeElement = elementUtils.getTypeElement(Constants.ROUTE_ROOT);

TypeElement iRouteGroupTypeElement = elementUtils.getTypeElement(Constants.ROUTE_GROUP);

//Map> routes

ParameterizedTypeName routes = ParameterizedTypeName.get(

ClassName.get(Map.class),

ClassName.get(String.class),

ParameterizedTypeName.get(

ClassName.get(Class.class),

WildcardTypeName.subtypeOf(ClassName.get(iRouteGroupTypeElement))

)

);

//参数 Map routes

ParameterSpec rootParameterSpec = ParameterSpec.builder(routes, "routes").build();

//函数 public void loadInto(Map> routes> routes)

MethodSpec.Builder loadIntoMethodBuilder = MethodSpec.methodBuilder(Constants.METHOD_LOAD_INTO)

.addAnnotation(Override.class)

.addModifiers(Modifier.PUBLIC)

.returns(TypeName.VOID)

.addParameter(rootParameterSpec);

//函数体

for (String key : rootMap.keySet()) {

loadIntoMethodBuilder.addStatement("routes.put($S, $T.class)",

key,

ClassName.get(Constants.PACKAGE_OF_GENERATE_FILE, rootMap.get(key)));

}

String rootClassName = Constants.NAME_OF_ROOT + moduleName;

JavaFile.builder(Constants.PACKAGE_OF_GENERATE_FILE,

TypeSpec.classBuilder(rootClassName)

.addSuperinterface(ClassName.get(iRouteRootTypeElement))

.addModifiers(Modifier.PUBLIC)

.addMethod(loadIntoMethodBuilder.build()).build())

.build().writeTo(filerUtils);

Log.i("Generated RouteRoot: " + Constants.PACKAGE_OF_GENERATE_FILE + "." + rootClassName);

}

上边的moduleName是在init方法中初始化的

Map options = processingEnvironment.getOptions();

if (!Utils.isEmpty(options)) {

//Constants.ARGUMENTS_NAME = "moduleName"

moduleName = options.get(Constants.ARGUMENTS_NAME);

}

processingEnvironment能够接收的参数需要在getSupportedOptions方法中定义,也可以用注解的方式定义,这里在定义RouteProcessor的时候使用@SupportedOptions(Constants.ARGUMENTS_NAME)

我们需要在依赖route_core库的 组件的build.gradle的添加这个参数:

javaCompileOptions{

annotationProcessorOptions {

arguments = [moduleName : project.getName()]

}

}

generatedRoot方法根据rootMap来创建一个IRouteRoot接口的实现类,这个接口主要是记录每个组名对应Group类。同样在编码前也是先写一个实现类Router$$Root$$impl,然后对比着进行编码。

public class Router$$Root$$impl implements IRouteRoot {

private Map> map;

@Override

public void loadInto(Map> routes) {

for (String groupName : map.keySet()) {

routes.put(groupName, map.get(groupName));

}

}

}

撰写完成后记得把这个类注释掉。注解和注解处理器我们都写完了,我们写一个demo看看效果,这里举一个setting的demo,目录结构如下:

setting_module.png

编译后生成的文件如下

setting_build.png

public class Router$$Group$$business implements IRouteGroup {

@Override

public void loadInto(Map atlas) {

atlas.put("/business/news", RouteMeta.build(RouteMeta.Type.ACTIVITY,BusinessNewsActivity.class, "/business/news", "business"));

atlas.put("/business/other", RouteMeta.build(RouteMeta.Type.ACTIVITY,BusinessOtherActivity.class, "/business/other", "business"));

}

}

public class Router$$Group$$personal implements IRouteGroup {

@Override

public void loadInto(Map atlas) {

atlas.put("/personal/wallet", RouteMeta.build(RouteMeta.Type.ACTIVITY,MyWalletActivity.class, "/personal/wallet", "personal"));

atlas.put("/personal/userInfo", RouteMeta.build(RouteMeta.Type.ACTIVITY,UserInfoActivity.class, "/personal/userinfo", "personal"));

}

}

public class Router$$Root$$setting implements IRouteRoot {

@Override

public void loadInto(Map> routes) {

routes.put("business", Router$$Group$$business.class);

routes.put("personal", Router$$Group$$personal.class);

}

}

这里看到生成的类分别实现了IRouteRoot和IRouteGroup接口,并且实现了loadInto()方法,而loadInto方法通过传入一个特定类型的map就能把分组信息放入map里,只要分组信息存入到特定的map里后,我们就可以随意的从map里取路由地址对应的Activity.class做跳转使用。

路由框架的初始化

我们要实现一个路由框架,就要考虑在合适的时机拿到这些映射文件中的信息,以供上层业务做跳转使用。那么在什么时机去拿到这些映射文件中的信息呢?首先我们需要在上层业务做路由跳转之前把这些路由映射关系拿到手,但我们不能事先预知上层业务会在什么时候做跳转,那么拿到这些路由关系最好的时机就是应用程序初始化的时候。另外如何去拿这些路由信息呢?在上面已经介绍过IRouteRoot接口的所有实现文件里保存着各个module的分组文件(分组文件就是实现了IRouteGroup接口的类文件),那么只要拿到所有实现IRouteGroup接口的类的集合,就可以根据path实现页面跳转了。

public class MyApplication extends Application {

@Override

public void onCreate() {

super.onCreate();

Router.init(this);

}

}

route_core module中的Router.java

public static void init(Application application){

context = application;

try {

loadInfo();

} catch (Exception e) {

Log.e(TAG, "初始化失败!", e);

e.printStackTrace();

}

}

private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

//1.扫描apk中的所有dex文件找出使用注解生成的类

Set routeMap = ClassUtils.getFileNameByPackageName(context, Router.ROUTE_ROOT_PACKAGE);

if (routeMap == null) {

return;

}

for (String className : routeMap) {

if (className.startsWith(ROUTE_ROOT_PACKAGE+"."+SDK_NAME+SEPARATOR+SUFFIX_ROOT)) {

// root中注册的分组信息,将分组信息加入仓库中

((IRouteRoot)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);

}

}

}

我们首先通过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)得到apt生成的所有实现IRouteRoot接口的类文件集合,通过上面的讲解我们知道,拿到这些类文件便可以得到所有的路由地址和Activity映射关系。

路由跳转实现

经过前面的介绍,我们已经能够在app启动的时候获得所有的路由信息,接下来就可以实现跳转了。

在app module的Mainctiviy中如下使用:

public void personalInfoJump(View view) {

Router.getInstance().build("/personal/userInfo").navigation();

}

public void businessNewsJump(View view) {

Router.getInstance().build("/business/news").navigation();

}

build的时候根据path路径得到一个postCard对象,然后调用Postcard的navigation()方法完成跳转。Postcard的内容如下:

public class Postcard extends RouteMeta {

private Bundle extras;

private int flag = -1;

private Bundle optionCompat;

private int enterAnim, exitAnim;

...

public Object navigation() {

return navigation(null, null);

}

public Object navigation(Context context) {

return navigation(context, null);

}

public Object navigation(Context context, NavigationCallback callback) {

return Router.getInstance().navigation(context, this, -1, callback);

}

...

}

下面看一下Router 的navigation核心功能:

public Object navigation(Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback){

prepareCard(postcard);

if (callback != null) {

callback.onFound(postcard);

}

switch (postcard.getType()) {

case ACTIVITY:

final Context currentContext = context==null?Router.context:context;

final Intent intent = new Intent(currentContext, postcard.getDestination());

intent.putExtras(postcard.getExtras());

if (postcard.getFlag()!=-1) {

intent.setFlags(postcard.getFlag());

}else if (!(currentContext instanceof Activity)){

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

}

new Handler(Looper.getMainLooper()).post(new Runnable() {

@Override

public void run() {

if (requestCode>0) {

ActivityCompat.startActivityForResult((Activity) currentContext, intent,

requestCode, postcard.getOptionCompat());

}else {

ActivityCompat.startActivity(currentContext, intent, postcard.getOptionCompat());

}

if ((postcard.getEnterAnim()!=0||postcard.getExitAnim()!=0) && currentContext instanceof Activity) {

((Activity)currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());

}

if (callback != null) {

callback.onArrival(postcard);

}

}

});

break;

}

return null;

}

prepareCard的实现如下:

private void prepareCard(Postcard card){

RouteMeta routeMeta = Warehouse.routes.get(card.getPath());

if (routeMeta == null) {

Class extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(card.getGroup());

if (groupMeta == null) {

throw new NoRouteFoundException("没找到对应路由: " + card.getGroup() + " " +

card.getPath());

}

IRouteGroup iRouteGroup;

try {

iRouteGroup = groupMeta.getConstructor().newInstance();

} catch (Exception e) {

throw new RuntimeException("路由分组映射表记录失败.", e);

}

iRouteGroup.loadInto(Warehouse.routes);

Warehouse.groupsIndex.remove(card.getGroup());

prepareCard(card);

}else {

card.setDestination(routeMeta.getDestination());

card.setType(routeMeta.getType());

}

}

这段代码Warehouse.routes.get(card.getPath())通过path拿到对应的RouteMeta,这个RouteMeta里面保存了activityClass等信息。继续往下看,如果判断拿到的RouteMeta是空,说明这个路由地址还没有加载到map里面(初始化时为了节省性能,只会加载所有的分组信息,而每个分组下的路由映射关系,会使用懒加载,在首次用到的时候去加载),只有在第一次用到当前路由地址的时候,会去Warehouse.routes里面拿routeMeta,如果拿到的是空,会根据当前路由地址的group拿到对应的分组,通过反射创建实例,然后调用实例的loadInto方法,把它里面保存的映射信息添加到Warehouse.routes里面,并且再次调用prepareCard(card),这时再通过Warehouse.routes.get(card.getPath())就可以顺利拿到RouteMeta了。进入else{}里面,调用了card.setDestination(routeMeta.getDestination()),这个setDestination就是将RouteMeta里面保存的activityClass放入Postcard里面。

prepareCard()方法调用完成后,我们的postcard里面就保存了activityClass,然后switch (postcard.getType()){}会判断postcard的type为ACTIVITY,然后通过ActivityCompat.startActivity启动Activity。到这里,路由跳转的实现已经讲解完毕了。

结束语

关于组件之间传递数据,可以参考ARoute添加Extra 注解,ExtraProcessor处理器以及IExtra接口。

通过手写ARoute我们会学到注解,注解处理器,JavaPoet和组件化思路,编写框架的思路等等。

android组建之间通信_Android组件化(三)组件之间的通信相关推荐

  1. 谈谈我对前端组件化中“组件”的理解,顺带写个Vue与React的demo

    谈谈我对前端组件化中"组件"的理解,顺带写个Vue与React的demo 前言 前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的APP的 ...

  2. android 多态如何组件化,Android组件化之子模块之间通信方案

    1 背景 Android开发中你的模块(Module)一般只有一个app主模块,随着功能不断扩展你会发现一个模块的缺点就是各种业务高度耦合,你就想测试登录模块,那么你可能会把支付模块也编译进去了,代价 ...

  3. Android组件化之组件通信

    Demo地址:https://github.com/751496032/ComponentDemo 本文是续上一篇Android组件化方案实践与思考文章一些思考,主要是针对组件间通信,比如: 每个组件 ...

  4. Android组件化专题 - 组件化配置

    demo地址 Android组件化专题,详细讲解组件化的使用及配置,以及实现的原理. 本文章讲解了组件化的由来及配置,下期讲解页面路由跳转及路由原理与apt 1. 组件化的由来 模块化.组件化和插件化 ...

  5. Reac组件化以及组件通信

    一.模块与组件以及模块化与组件化慨念 模块:向外提供特定功能的JS文件,便于复用JS,简化JS,提升JS效率数据.对数据的操作(函数).将想暴露的私有的函数向外暴露(暴露的数据类型是对象) 2. 模块 ...

  6. android 组件化 蘑菇街,组件化在蘑菇街

    零.说点什么吧 周末是一个轻松的日子,于是决定写点什么.名字取得比较大,暂时也没有想到应该怎么命名.刚刚开始仔细的看了一下 MGJRouter 中的代码,所以一边看,也就一边的做点笔记.现在看完了,整 ...

  7. android pod 组件化_Flutter组件化导入至iOS现有工程中(CocoaPods篇)

    Flutter项目实战:方案有三 纯Flutter项目,需要投入大量人力进入到Flutter编程中,且现有原生项目需要完全摒弃 Flutter项目混编,暂且不说Flutter所支持的框架,与原生交互的 ...

  8. Vue源码学习 - 组件化(三) 合并配置

    Vue源码学习 - 组件化(三) 合并配置 合并配置 外部调用场景 组件场景 总结 学习内容和文章内容来自 黄轶老师 黄轶老师的慕课网视频教程地址:<Vue.js2.0 源码揭秘>. 黄轶 ...

  9. 高仿富途牛牛-组件化(三)-界面美化

    文章目录 一.概述 二.效果展示 三.工具箱 1.布局 a.标题栏 b.客户区 2.功能详解 四.组件模板工具栏 五.其他界面美化 六.使用qss文件 七.相关文章 一.概述 今天是组件化的第三篇文章 ...

  10. vue的组件化+父子组件的通信

    文章目录 1.注册组件的基本步骤 1.1 原始的创建方法 1.2语法糖创建方法 2.全局组件与局部组件 2.1 全局组件 2.2 局部组件 2.3语法糖局部组件(最简写法) 3. 父子组件 4.组件中 ...

最新文章

  1. 【linux】修改机器时间
  2. Phoenix报错(6)Inconsistent namespace mapping properites
  3. oppo手机工程模式清除数据需要密码_手机隐藏的快捷键都有哪些?
  4. python wget安装_Macbook系统环境安装wget的2个方法 - 传统包及Homebrew安装
  5. 面试后要请你吃饭_面试问同事请吃饭唯独不叫你咋办?小伙说这是好机会,当场被录取...
  6. i/o timeout , 希望你不要踩到这个net/http包的坑
  7. Eclipse EGit插件安装使用记录
  8. Template parse errors: The pipe 'translate' could not be found
  9. cmd命令查看服务器硬盘序列号,硬盘序列号查询命令_Win7系统中怎么通过CMD查看硬盘序列号...
  10. WIN10系统下命令提示符(cmd)的基本操作
  11. windows”出现身份验证错误,要求的函数不正确“的解决方法
  12. 最小生成树详细讲解(Prime算法+Kruskalsuanfa)
  13. mysql 正则表达式区间,MySQL——使用正则表达式查询
  14. [Practical.Vim(2012.9)].Drew.Neil.Tip94 学习摘要
  15. 视频转动态图片gif怎么制作?教你一招轻松转换
  16. Mybatis CRUE,Mybatis 更多查询
  17. 2011年数学建模国赛B题(交巡警服务平台的设置与调度模型)论文.doc
  18. AMD的cpu和Windows11系统下安装ensp,启动NE40E设备失败解决办法
  19. 基于opencv实现人脸猫脸图像检测(python)
  20. 将一个大于4的正整数分解为连续的正整数之和,请显示全部分解结果。

热门文章

  1. java hive查询,hive查询报错
  2. 四川大学 设计专业 C语言必修,四川大学C语言2001年真题_跨考网
  3. oracle数据库配置管理,Oracle配置管理
  4. pyqt生成 android,PyQt on Android
  5. c语言字面量的作用是为变量,C语言(五) C 全局变量,局部变量,静态变量和常量...
  6. 单片机串口通信学号显示_触摸屏与单片机串口通信测试
  7. w3c html5 客户端缓存数据格式,Html5应用程序缓存(Cache manifest)
  8. VMware Workstation虚拟机窗口小,无法显示内部系统全部桌面
  9. 运维人员mysql如何访问_mysql 运维常见操作
  10. cgi web 调用多次启动_全面了解CGI、FastCGI、PHPFPM