Android组件化手撸实现步骤

       在App功能逐渐冗杂的今天,衍生出很多优化方案,组件化作为其中一种方式也被广泛应用。抛开那些第三方,手撸一次实现组件化的通信,也更利于学习第三方的开源框架,下面来一步一步实现。
一、准备工作

       1. 首先我在创建完project后,除了默认创建的app model,另外创建了两个model:LoginComponent和MineComponent。(component就是组件的意思,相信来到这里的朋友都是了解过组件化原理和一些相关术语,这里就不再赘述)

       2. 在创建完两个单独的component后,创建一个Base Library,因为相互通信的需要。

       3. 创建后的目录就是这样的:


Android组件分布.png

model.PNG
二、配置依赖环境和分离运行需求

       这里的配置环境就是在gradle里做一些相关操作,主要有如下操作:

  1. 统一每个model之间的sdk相关配置;
  1. 将model的两种运行方式进行分离,也就是分开处理,如果有资源文件也同样需要分开处理
  2. 分离的内容通常包括如下几种:
  • apply运行方式的分离
  • applicationId的分离
    以上两种都是在component gradle里操作
  • app model引用library model的分离
    以上一种是在app model里操作
  • AndroidManifest(清单文件)的分离
  1. 将所有model都对baseLibrary依赖

以上分离的目的是区分application和library两种运行方式所对应的需求,
而依赖的目的就是互相通信。


\color{red}{ 下面就来逐一介绍 }

        1. 在app的gradle.properties里,配置如下(如果有其他需求一样可以增添):
#全局配置gradle环境
compile_sdk_version = 28
min_sdk_version = 23
target_sdk_version = 28
support_sdk_version =  28.0.0
constraint_sdk_version = 1.1.3

#配置单个model是否可独立运行
loginRunAlone = false
mineRunAlone = false
       2. 在\color{red}{每个component的build.gradle里}引用如上配置。

以下是引用配置和分离的截图:
除library model和主app model(也就是创建项目时自动创建的那个model)不需要进行application和library的判断,因为第一个始终以library运行;第二个始终以application运行。其他的component皆需考虑是否以单独的application运行的情况。

对上面文字清晰地解释一下吧。下面三种颜色,代表的在gradle里三种需要配置的事务:
    红色框:apply的运行方式的分离;
    绿色框:sdk版本的统一配置;
    黄色框:applicationId的分离,component以application运行需要ID;

其中红色框和黄色框部分只需要在component里配置,library和app里都不需要;
绿色框的内容则试用于所有gradle,能配置sdk的地方都可以。

gradle配置.png
         3. app model引用library model的分离

在不以单独application运行的情况下,把component作为library依赖且运行。

app_model分离依赖.png
         4. AndroidManifest的分离

首先很明确,library和application的清单文件内容是不需要的,除了保存必有的包名(package)和四大组件的注册信息。

首先,切换project目录,因为在Android目录下看不到的。


切换目录为project展示.png

然后,给所有component创建另外一个目录的AndroidManifest文件,我这里在main/下创建的文件夹和文件,跟Java文件夹同级,也可以创建其他目录,后续加载路径。
(下图中带main下的是自动创建的,作用于application运行;manifest下的是刚才我创建的,作用于library运行)


创建清单文件.png

接下来,来到作用于library运行的清单文件,干掉用不着的只剩包名和四大组件注册信息(我这里只有activity示例),如下图:


整理作用于library的清单文件.png

来,最后一步--------------分开加载清单文件
来到所有component的gradle里如下配置:


分离清单文件.png

当然注意的是,判断运行状态是根据component的,因为gradle对groovy好像不太友好,没有提示,记得改一下。顺便提一下,maniFest不要写成了mainFest,相信大家不会犯我这种低级错误。当然如果你文件名一致无所谓。


拼写错误.png
         5. 各个model对baseLibrary的依赖
app_model引用baseLibrary.png
login_model引用baseLibrary.png

mine_model引用baseLibrary.png
只至此呢,所有的配置都搞完了。下面需要搭建交互逻辑!
三、搭建model间交互逻辑

        1. 来到baseLibrary下,新建接口IBaseAppComponent,写入方法initialize(Application application),不知道不要紧,后面就知道干啥了。

 public interface IBaseAppComponent {
    void initialize(Application application);
}

        2. 让app和另外两个component的自定义application都实现IBaseAppComponent 接口。

    // MainAppcation 
public class MainAppcation extends Application implements IBaseAppComponent {

    private static MainAppcation app;
    public static MainAppcation getInstance(){ return app;}

    @Override
    public void onCreate() {
        super.onCreate();
        initialize(this);
    }

    @Override
    public void initialize(Application application) {
        app = (MainAppcation) application;// 全局统一的上下文
    }

在component的onCreate方法里手动调用,则是考虑到app model单独运行时需要调用。

  // LoginApplication 
public class LoginApplication extends Application implements IBaseAppComponent {

    private static Application app;
    public static Application getInstance(){return app;}
    @Override
    public void onCreate() {
        super.onCreate();
        initialize(this);
    }

    @Override
    public void initialize(Application application) {
        app = application;
    }
}
  1. 回到MainAppcation的initialize方法里,因为有了一层实现关系,我们可以在这个方法里通过library的接口实现调用component内的方法,并传递MainApplication的context。
    @Override
    public void initialize(Application application) {
        app = (MainAppcation) application;

        try {
            for (String app : AppComponontConfig.COMPONENTS) {
                // COMPONENTS是一个AppComponontConfig类里的静态字符串数组----->
                    // ,包含了所有component的包名
                // 用包名反射,判断是否实现了IBaseAPPComponent
                Object aClass = Class.forName(app).newInstance();
                if (aClass instanceof IBaseAppComponent) {
                     // 确保component实现了此接口 
                    ((IBaseAppComponent) aClass).initialize(this);// ---->
                     // 这里主app的在整体运行时需要提供全局统一的上下文
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
  1. 继续在library model里,创建ServiceFactory类, 用来管理model间交互的引用对象实例。
public class ServiceFactory {

    private static final ServiceFactory instance = new ServiceFactory();
    public static ServiceFactory getInstance() {
        return instance;
    }

    // 为组件化接口提供get及set方法
    private I_LoginService mLoginService;
    private I_MineService mMineService;

    public I_LoginService getLoginService() {
        // 避免在移除组件后报错。这里不返回null,返回一个空实现类
        if (mLoginService == null) {
            mLoginService = new EmptyLoginServiceImpl();
        }
        return mLoginService;
    }

    public void setLoginService(I_LoginService loginService) {
        mLoginService = loginService;
    }

    public I_MineService getMineService() {
        if (mMineService == null) {
            mMineService = new EmptyMineServiceImpl();
        }
        return mMineService;
    }

    public void setMineService(I_MineService mineService) {
        mMineService = mineService;
    }
}
  1. 在component里创建业务类LoginServiceImpl(这里只拿其中一个component举例,另外的同理)实现I_LoginService接口。
public class LoginServiceImpl implements I_LoginService {
    @Override
    public void launch(Context contex) {
        // 每个组件都提供一个launch方法用来启动该组件
    }
}
  1. 在component的application里(这里同样拿其中一个举例)绑定传递对象
@Override
    public void initialize(Application application) {
        app = application;
        ServiceFactory.getInstance().setLoginService(new LoginServiceImpl());
    }

那么我们的准备也就到此结束了,下面我将准备实现两个功能,以测试是否成功和效果。


四、实现交互逻辑

终于来到这里了对吧!
先说下我要实现的功能:

  1. main跳转至mine界面
  2. mine再跳转至login界面
  3. login再调用mine组件中的方法展示一个fragment
    那好,开整!
1. 来到login组件的实现类,前面代码里提到过我们为每个组件提供了一个launch方法,这个方法用来其他组件启动本组件。里面自然做了启动的操作。
public class LoginServiceImpl implements I_LoginService {
    @Override
    public void launch(Context context) {
        Intent intent = new Intent(context, LoginActivity.class);
        context.startActivity(intent);
    }
}
2. 来到Mine组件的业务实现类,同样提供给外部组件的启动方法和login组件显示fragment的方法
public class MineServiceImpl implements I_MineService {
    /**
     * 提供外部组件启动方法
     * @param context context
     */
    @Override
    public void launch(Context context) {
        Intent intent = new Intent(context, MineActivity.class);
        context.startActivity(intent);
    }

    /**
     * 显示fragment的方法
     */
    @Override
    public Fragment showFragment(FragmentManager manager, int viewId, Bundle bundle) {
        TestFragment testFragment = new TestFragment();
        testFragment.setArguments(bundle);
        return testFragment;
    }
3. 在Mine model中的Fragment页面,这里我们把login组件中调用时传的内容展示出来 。
public class TestFragment extends Fragment {
    private View mView;
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mView = inflater.inflate(R.layout.fragment_test, null);
        return mView;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle arguments = getArguments();
        ((TextView) mView.findViewById(R.id.tv_fr)).setText(arguments.getString("string"));
    }
}
4. 来到app model的Activity,我们先写好第一个的测试代码
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv = findViewById(R.id.sample_text);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ServiceFactory.getInstance().getMineService().launch(MainActivity.this);
            }
        });
    }
5. 再来到Mine model的Activity,搞定第二个测试
public class MineActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mine);
        findViewById(R.id.tv_go_to_login).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                  // 跳转到Login界面
                  ServiceFactory.getInstance().getLoginService().launch(MineActivity.this);
            }
        });
    }
}
6. Login页第三个测试,都是些没什么技术含量的代码应该没同学看不懂。
public class LoginServiceImpl implements I_LoginService {
    @Override
    public void launch(Context context) {
        Intent intent = new Intent(context, LoginActivity.class);
        context.startActivity(intent);
    }
}
OK,跑一个
组件化Demo录屏.gif

成功地进行了三个页面的跳转,并在一个组件中调用另一个组件的方法,展示了一个组件携带到另一个组件的数据。

总结四个自己挖的坑
  1. MainApplication忘记在清单文件配置name,其他两个组件因为是反射调用的,所以没有进行name配置
  2. 判空方法写在了setLoginService里
  3. 在gradle里加载AndroidManiFest,写成了AndroidMainFest
  4. 修改了两个组件的application类名,忘记在AppComponontConfig.COMPONENTS修改,导致instanceOf始终走了异常
另外提醒一下,在组件和app model里资源名字一样时,组件里的activity加载的是app model里的资源,需要加上标识。
五、小结一下

组件化在最初的实现是比较繁琐的一件事,特别是在搭建项目通信环境,即使在使用第三方的情况下。所以,在开发过程中依据业务特性进行项目架构选择,分析出各种优缺点才是最好的。

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

推荐阅读更多精彩内容