Android App模块化篇

还是要从头说起:

最近由于项目进行到了一定的阶段,项目中的代码到达了一定的体积与当量(主要是业务越来越多),这个时间分层使用MVC,MVVM等来分层已经力不从心,不是说MVC不行而是我认为MVC分层是一种粒度相对于较小的分层,相对于模块化要站在一个更大更宏观的角度来进行项目架构的调整所以这次一定要从项目全局宏观的角度出发。

上一篇《IOS App模块化篇》已经介绍了 模块化,组件化,插件化 的概念这里我们继续来学习下这三个概念:

插件化:
插件化开发是将一个项目app拆分成多个模块,这些模块包括宿主和插件。每个模块相当于一个apk,最终发布的时候将宿主apk和插件apk单独打包或者联合打包。
作用:每个组负责一个插件,彼此之间没有过多的依赖,可以单独调试打包,有时发版其实就相当于发插件,最重要的是可以动态下载插件更新插件。

模块化:
组件化开发是将一个项目app拆分成多个模块,模块化开发过程中相互依赖或单独调试,最终发布的时候是将这些模块合并统一成一个apk,另外模块化也是插件化的前提

组件化:是一种细粒度更小的结构方式多见于项目中一个组件化控件例如:自定义Button按钮,这种组件更多是为了在项目中的复用以及方便开发管理。

好了,概念是比较通俗浅显易懂的,如果有疑问可以留言讨论,我们继续讨论Android App的模块化路上会遇到的问题,以及解决的方法,问题还是那几个通用的问题:

 1. 多个模块的跳转怎么解决 
 2. 模块化里面的传值问题怎么解决
 3. 模块化里面的方法互相怎么调用
 4. 模块化里面的响应式事件怎么处理

问题虽多但是不得不说在Android模块化框架的选型上面确没有那么多烦恼,因为阿里开源的Arouter框架确实是受众不少,而且我们这4个问题都可以解决,确切的说是解决了前3个最后一个可以使用EventBus来实现。(插一句我找了一番没有发现IOS里面有EventBus这种知名度高而且好用的注册监听框架

那么我们来看看Arouter的功能介绍:

支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
支持多模块工程使用
支持添加多个拦截器,自定义拦截顺序
支持依赖注入,可单独作为依赖注入框架使用
支持InstantRun
支持MultiDex(Google方案)
映射关系按组分类、多级管理,按需初始化
支持用户指定全局降级与局部降级策略
页面、拦截器、服务等组件均自动注册到框架
支持多种方式配置转场动画
支持获取Fragment
完全支持Kotlin以及混编
支持第三方 App 加固(使用 arouter-register 实现自动注册)
支持生成路由文档
提供 IDE 插件便捷的关联路径和目标类

功能非常多,那么我们来看一看基本使用,看完使用我们再说实现原理

@Route(path = "/home/main")
public class MainActivity extends AppCompatActivity {

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

注意下第一句话 @Route(path = "/home/main") 这个注解意思就是指定了这个 MainActivity 的URL标识,如果要跳转的话一句话搞定:

ARouter.getInstance().build("/home/main").navigation();

是不是很简单,那跳转的参数怎么传呢,也很简单:

ARouter.getInstance().build("/chat/main").withString("key", "888").navigation();

这样传参那怎么取呢,取的模块也很简单也是通过注解来承接参数:

@Route(path = "/chat/main")
public class MainActivity extends AppCompatActivity {

    private TextView text;

    @Autowired
    public String key;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        ARouter.getInstance().inject(this);
        Toast.makeText(this, "收到传送过来的数据:" + key, Toast.LENGTH_LONG).show();
        text = findViewById(R.id.text);
        text.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onBackPressed();
            }
        });
    }
}

第1,2问题得到解决了,那么第3个问题怎么解决呢,Arouter里面提供一种叫做IProvider的对象你去继承这个对象,

public interface HomeExportService extends IProvider {
    String sayHello(String s);
    BaseHomeModel getHomeModelData();
}

实际的业务模块实现了这个接口,首先通过注解来标识这个服务类:

@Route(path = "/home/HomeService",name = "测试服务"):

@Route(path = "/home/HomeService",name = "测试服务")
public class HomeService implements HomeExportService {
    private String name;
    @Override
    public String sayHello(String s) {
        return "HomeService say hello to" + s;
    }

    @Override
    public void init(Context context) {
       initData();
    }

    private void initData() {
        name="haozi";
    }

    public BaseHomeModel getHomeModelData(){

        BaseHomeModel hm = new HomeModel("home",100);
        return  hm;
    }
}

调用的方式为首先用注解来找到这个服务类,然后进行调用

@Autowired(name = "/home/HomeService")

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView chat_tv;
    private TextView contract_tv;
    private TextView find_tv;
    private TextView mine_tv;
    private TextView say_hello_tv;

    @Autowired(name = "/home/HomeService")
    public HomeExportService baseService;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ARouter.getInstance().inject(this);
        initView();
        BaseHomeModel bm =  baseService.getHomeModelData();
    }
}

这里面其实也有分层的概念在里面服务模块与调用模块分开互不影响,然后中间有一个中间层暴露服务的申明,然后服务模块与调用模块分别引用了这个中间层模块

OK,目前到此为止Arouter+EventBus能解决我们4个问题,看起来很美好,那么它是怎么做到的呢?···

原理:

其实Arouter的原理的文章网络上还是比较多的,但是我用一种比较通俗易懂的方式来给大家做一个讲解,主要分两个部分:

1.首先APT是什么?

APT:APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

简单的来讲通过APT技术,即注解处理器在编译时扫描并处理注解,注解处理器
可以在编译时生成额外的.java文件,在程序运行的时候调用相关方法,可以达到减少重复代码的效果。它的好处:提高开发效率,使得项目更容易维护和扩展,同时几乎不影响性能。

2.Arouter基于APT是怎么做的?

那么ARouter背后是怎么样实现跳转的呢?我们在代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activity.class映射关系的类文件,例如app模块编译动态生成的文件如下:

image.png

其中 Aroute-Root-app 文件内容是看你这个模块定义的URL 例如有两个:/test/main/aaa/main 那么 Aroute-Root-app 里面就有两条数据如下:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("aaa", ARouter$$Group$$aaa.class);
    routes.put("test", ARouter$$Group$$test.class);
  }
}

分别保存的是他们 Group 类的名字,Group 就会有两个文件:

image.png

每个 Group 里面的内容保存你想跳转的Activity的类:

atlas.put("/aaa/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/aaa/main", "aaa", null, -1, -2147483648));
atlas.put("/test/target", RouteMeta.build(RouteType.ACTIVITY, TargetActivity.class, "/test/target", "test", new java.util.HashMap<String, Integer>(){{put("key3", 8); }}, -1, -2147483648));

如果你的模块两个URL是:
/test/main/test/main 那么你的 Aroute-Root-app 文件里面的内容只有一条数据

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("test", ARouter$$Group$$test.class);
  }
}

然后你的 Group 里面会有两条数据,保存你想跳转的Activity的类的名字:

public class ARouter$$Group$$test implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/test/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/test/main", "test", null, -1, -2147483648));
    atlas.put("/test/target", RouteMeta.build(RouteType.ACTIVITY, TargetActivity.class, "/test/target", "test", new java.util.HashMap<String, Integer>(){{put("key3", 8); }}, -1, -2147483648));
  }
}

如果你没有特定指定组的话,它其实是用URL的前 /test/ 部分来作为一个 Group 的,如果你这个模块没有使用 IProvider 提供对外模块的服务则 Atoute-Provider-app 文件夹里面的内容为空

然后app进程启动的时候会加载这些类文件,把保存这些映射关系的数据读到内存里(保存在map里),然后在进行路由跳转的时候,通过build()方法传入要到达页面的路由地址,ARouter会通过它自己存储的路由表找到路由地址对应的Activity.class(activity.class = map.get(path)),然后new Intent(context, activity.Class),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法,它的内部会调用startActivity(intent)进行跳转,这样便可以实现两个相互没有依赖的module顺利的启动对方的Activity了。

gradle里面这一行说明了使用了注解解释器

annotationProcessor com.alibaba:arouter-compiler:1.1.4

这一行说明了自定义的注解以及一些注解相关的工具类服务类在这里面

implementation com.alibaba:arouter-api:1.3.1

这几行是为了给注解处理器提供模块名字,注解处理器会根据模块名字来动态生成对应的JAVA文件名例如下图:结尾home就是模块名字

javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }
image.png

PS:顺便插一句很多博客没有讲清楚下面这句话的作用就在代码加上了,其实这句话的意思是如果你的类里面需要用到@Autowired 来进行注入参数以及服务的话就需要手动调用这句话,没有的话则不需要也是可以的。

ARouter.getInstance().inject(this);

其实网络上有很多已经手动实现了APT的功能一可以给大家推荐几个链接:
//www.greatytc.com/p/b5be6b896a1a
//www.greatytc.com/p/857aea5b54a8

已经写得比较好了,有疑问可以给我留言大家一起交流

我已经在一个Demo的基础上做了一层小小的改进,改进主要是HomeExportService部分,Service部分是在Commlib模块里面用作暴露给外面调用的,如果需要返回模型对象的话应该怎么处理呢?我在Commlib公共模块里面定义了一个model文件夹和service文件夹平级,里面包含了service服务要返回给调用模块的model例如BaseHomeModel,然后真正实现服务的模块里面的model再继承这个model:

image.png
public class BaseHomeModel {

    public String add;
    public  int count;

    public BaseHomeModel(String add, int count) {
        this.add = add;
        this.count = count;
    }

    public String getAdd() {
        return add;
    }

    public void setAdd(String add) {
        this.add = add;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

public class HomeModel extends BaseHomeModel{

    private String add;
    private  int count;

    public HomeModel(String add, int count) {
        super(add, count);
        this.add = add;
        this.count = count;
    }

    public String getAdd() {
        return add;
    }

    public int getCount() {
        return count;
    }

    public void setAdd(String add) {
        this.add = add;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

然后需要调用的模块只需要依赖Commlib模块,首先先用注解获得service的注入

@Autowired(name = "/home/HomeService")
public HomeExportService baseService;

这里面会注入真正提供的service服务对象在里面,上面介绍了这个对象使用了 @Route 注解早在编译期的时候已经加入到了map里面了

@Route(path = "/home/HomeService",name = "测试服务")
public class HomeService implements HomeExportService {
    private String name;
    @Override
    public String sayHello(String s) {
        return "HomeService say hello to" + s;
    }

    @Override
    public void init(Context context) {
       initData();
    }

    private void initData() {
        name="Hao zi";
    }

    public BaseHomeModel getHomeModelData(){

        BaseHomeModel hm = new HomeModel("home",100);
        return  hm;
    }
}

然后进行调用即可:

BaseHomeModel bm =  baseService.getHomeModelData();
Toast.makeText(this, bm.getAdd() + "|" + bm.getCount(), Toast.LENGTH_SHORT).show();

好了,我们Android App模块化篇讲解得差不多了,最后我会把Demo上传给大家,如果大家喜欢请留言或者点赞···

AndroidRouterDemo

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

推荐阅读更多精彩内容