Android組件化示例代碼github地址:https://github.com/respost/ModuleDemo
一、組件化初始模型
1、通過一個簡單的android專案初始架構圖來了解組件化,如下圖:

打個生動的比喻,把我們的APP當成一個電腦主機,那么app外殼就是主機外殼,main組件就是主板,其他各個組件就類似于硬碟、網卡、顯卡之類的東西,各個組件連接到主板上,然后再安裝到主機殼中,對外展示為一個完整的電腦主機,
2、app外殼和main組件是我們app的必備組成部分,一起構成了可對外發布的完整app,其他組件可以集成進來,也可以不集成進來,只會增加或者減少我們app的功能,但不影響我們app的最終發布,
二、創建Module模塊(組件)
在我們的實際專案中,組件展示出來的效果大概是這樣的:

1、我們開始創建Module模塊,專案的APP上右鍵 → New → Module

2、選擇 Android Library → Next

3、 輸入模塊名稱 → Finish

三、組件的build.gradle檔案說明
1、通過以上Module模塊的創建,包括main組件在內,一共有5個組件,所以對應的有5個組件build.gradle檔案,如下圖:

2、 main組件的gradle檔案中,apply plugin使用的是com.android.application

3、其他業務模塊(組件A、組件B、組件C、common組件等),apply plugin使用的是com.android.library

四、組件集成
各個組件都建立完成之后,接下來可以把組件集成到main組件中,集成非常簡單,只需在main組件的gradle檔案中添加dependencies{}配置,添加如下陳述句:
dependencies {
...
//集成組件A
implementation project(':modulea')
//集成組件B
implementation project(':moduleb')
//集成組件C
implementation project(':modulec')
}
如下圖:

五、組件資源共享
1、在common組件的build.gradle檔案中,添加android 配置,如下:
android {
//省略前面的代碼...
repositories {
flatDir {
dirs 'libs'
}
}
}
在各個需要呼叫公共common組件的組件build.gradle檔案中,也添加android 配置,如下:
android {
//省略前面的代碼..
repositories {
flatDir {
dirs '../common/libs/', 'libs'
}
}
}
2、common組件里引入各種類別庫的時候必須用api,而不是用implementation,原因:
implementation編譯的依賴只作用于當前的module,即common組件模塊中使用implementation編譯的三方庫只對common模塊起作用,main組件模塊中無法使用該三方庫,

3、關于組件資源共享,舉個簡單示例:例如圖片都是存放到公共的common組件的res里,那么如何在組件A、組件B、組件C里使用呢?

使用方法如下:
- 打開各組件的build.gradle檔案,在dependencies{}里添加如下代碼即可:
dependencies {
...
implementation project(':common')
}
- 如此一來,就能在組件A里呼叫common組件的圖片資源了

4、同樣的道理, 組件A、 組件B、 組件C的顏色代碼也可以直接呼叫公共common組件里colors.xml的代碼

5、我們可以把其他第三方庫、自定義view、工具類、公用資源都放進公共common組件里,也就是說組件A、組件B、組件C里build.gradle所引入的類別庫,都可以放到common組件里的dependencies{}里

6、所以各個業務組件里面的build.gradle檔案的dependencies{}配置簡化后,就變成了下面這樣:

7、通過以上解說,大家應該都明白了吧,圖片、xml這些(value目錄下的各種xml檔案),都可以放到公共common組件里,然后再被其他組件參考,對于全域共用的style.xml檔案,我們更應該把它放在common組件中,例如我們的專案theme,本來是放在main組件的style里面,我們可以把它移到common中,這樣其他組件除錯時,作為一個單獨的專案,也能和主專案有一樣的主題,總而言之,所有你認為可以被各個組件共享的資源,都可以放在common組件中,
六、往組件里添加Fragment
1、以組件A為例,在組件A里添加一個包fragment

2、在fragment包右鍵 → New → Fragment →Fragment(Blank)

3、 填寫Fragment碎片名稱,勾選創建xml檔案,如下:

4、對應的fragment_module_a.xml檔案代碼:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/a" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="5dp"
android:text="組件A"
android:background="@color/green"
android:textColor="@android:color/white"
android:textSize="24dp" />
</FrameLayout>

5、其他組件也類似組件A一樣,創建一個Fragment碎片,然后添加不同的背景圖片即可,
6、main組件里添加導航和Fragment容器,main組件里activity_main.xml的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/container"
android:orientation="vertical">
<FrameLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
</FrameLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?android:attr/windowBackground"
app:menu="@menu/navigation" />
</LinearLayout>
7、res下創建一個menu目錄,里面添加一個navigation.xml檔案,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_a"
android:icon="@drawable/home"
android:title="組件A" />
<item
android:id="@+id/navigation_b"
android:icon="@drawable/video"
android:title="組件B" />
<item
android:id="@+id/navigation_c"
android:icon="@drawable/me"
android:title="組件C" />
</menu>
navigation.xml里呼叫的icon圖片分別放到drawable和drawable-24目錄里,最終主APP的MainActivity界面如下圖:

8、MainActivity.java的代碼如下:
package net.zy13.module.demo;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import android.os.Bundle;
import android.view.MenuItem;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import net.zy13.module.modulea.fragment.ModuleAFragment;
import net.zy13.module.moduleb.fragment.ModuleBFragment;
import net.zy13.module.modulec.fragment.ModuleCFragment;
public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener {
//定義碎片集合
private Fragment[] fragments = new Fragment[3];
//當前顯示的fragment的索引位置
private int currentIndex = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFragment();
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(this);
}
/**
* 初始化Fragment碎片
*/
private void initFragment() {
if (fragments[0] == null) {
fragments[0] = new ModuleAFragment();
getSupportFragmentManager().beginTransaction().add(R.id.content, fragments[0], "moduleA").commit();
} else {
getSupportFragmentManager().beginTransaction().show(fragments[0]);
}
}
/**
* 導航選擇事件
* @param item
* @return
*/
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_a:
if (currentIndex == 0) return true;//如果已經是當前的fragment,不用切換
FragmentTransaction transition0 = getSupportFragmentManager().beginTransaction();
hideAndShow(0,transition0);
return true;
case R.id.navigation_b:
if (currentIndex == 1) return true;//如果已經是當前的fragment,不用切換
FragmentTransaction transition1 = getSupportFragmentManager().beginTransaction();
if (fragments[1] == null) {
fragments[1] = new ModuleBFragment();
transition1.add(R.id.content, fragments[1], "moduleB");
}
hideAndShow(1,transition1);
return true;
case R.id.navigation_c:
if (currentIndex == 2) return true;//如果已經是當前的fragment,不用切換
FragmentTransaction transition2 = getSupportFragmentManager().beginTransaction();
if (fragments[2] == null) {
fragments[2] = new ModuleCFragment();
transition2.add(R.id.content, fragments[2], "modulec");
}
hideAndShow(2,transition2);
return true;
}
return false;
}
/**
* 除了指定的fragment不hide,其他fragment全hide
* @param expectIndex 指定的fragment在fragments中的位置
* @param transition
*/
private void hideAndShow(int expectIndex,FragmentTransaction transition) {
for (int i = 0; i < fragments.length; i++) {
if (i != expectIndex && fragments[i] != null) {
transition.hide(fragments[i]);
}
}
transition.show(fragments[expectIndex]);
transition.commit();
currentIndex = expectIndex;
}
}
9、Run運行專案,最終效果如下圖,點擊導航,可以切換到對應的組件上:

七、各個組件單獨開發(測驗)
前面我們把各個組件集成到main組件中,現在我們把組件拆分出來,單獨開發,開發測驗完成后,再把組件集成到main組件中,最后發布,組件單獨出來開發的方法就是:在build.gradle檔案中,把apply plugin: 'com.android.library',改成apply plugin: 'com.android.application',也就是把其library模式改為application模式,因為只有application才可以單獨運行,library必須依靠application才能運行,
那么問題來了?
組件單獨開發時,我們需要改build.gradle的apply plugin模式,等要集成到main組件時,又得改回來,如果這樣子手工去改,組件一多,修改起來比較麻煩,也不優雅,優雅的解決辦法就是設定一個開關,打開時,就是application模式,可以單獨開發;關閉時,就是library模式,可以集成到main組件中,現在按我下面的步驟來實作:
1、在專案根目錄下,有一個build.gradle檔案,在這個檔案最末尾添加一個ext {}配置,然后在ext配置里設定一個常量isDebug,值設為true
ext {
/**
* 組件除錯模式
* isDebug = true 是組件模塊,說明是單獨的App
* isDebug = false是集成模式,說明是依賴Lib
* 每次更改“isDebug”的值后,需要點擊 "Sync Project" 按鈕
*
*/
isDebug = true
}

2、build.gradle里設定了isDebug常量后,我們專案中的其他build.gradle檔案都可以把這個常量讀取出來,所以我們可以在其他組件的build.gradle檔案中,讀取該常量的值,動態設定apply plugin,代碼如下:
if(isDebug){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
3、這樣子設定之后,當我們需要切換模式時,只需要修改專案根目錄下build.gradle檔案中isDebug常量的值,修改完成之后,點擊Project sync按鈕同步一下即可,如果有報錯,那么還有個地方需要修改一下,就是main組件的build.gradle檔案,我們把module的模式改成了application,main組件就不能引入application,引入的話就會報錯,所以當是debug除錯模式時,這里就不引入該組件,以免報錯,所以在集成組件前,要先判斷是什么模式,如下圖:

4、接下來還得修改 AndroidManifest.xml,當把一個module設定為application時,AndroidManifest.xml需要包含一個app所需要的屬性,例如app的icon、theme、launch Activity這些屬性設定,而當module為library時,這些屬性就都不需要用到,所以當我們處于不同模式時,AndroidManifest.xml檔案的配置也得不同,方法如下:
(1)、Android目錄模式切換到Project目錄模式

(2)、 在各個組件的src檔案夾中新創建一個debug目錄,再把我們用于debug除錯的AndroidManifest.xml檔案放進去

(3)、除錯用的AndroidManifest.xml檔案可以直接復制manifests目錄里的,然后添加application的基本資訊,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.zy13.module.modulea">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
</application>
</manifest>
以上內容會有很多錯誤提示,其實提示的無非就是資源找不到,既然前面我們已經創建了公共的common組件,那么我們只需要把main組件中相應的資源移動到common組件中就可以了,
5、接下來在各個組件的build.gradle檔案中,指定不同模式下使用的AndroidManifest.xml檔案,在android {}里添加如下代碼:
sourceSets {
main {
if (isDebug) {
manifest.srcFile 'src/debug/AndroidManifest.xml'
}else{
manifest.srcFile 'src/main/AndroidManifest.xml'
//集成開發模式下排除debug檔案夾中的所有Java檔案
java {
exclude 'debug/**'
}
}
}
}

6、以上設定完成,并且sync project(同步專案)之后,各個組件會是這樣的目錄結構:

7、在各個組件里創建一個用于除錯啟動的MainActivity活動,然后把fragment加載到activity_main.xml里,所對應的activity_main.xml布局檔案如下:
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="net.zy13.module.modulea.fragment.ModuleAFragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>
8、添加MainActivity活動后,我們還需要手動設定該活動為入口,打開src/debug/目錄下的AndroidManifest.xml檔案,修改MainActivity的配置如下:
<activity
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
9、以上步驟完成之后,組件就可以單獨作為一個app來開發測驗了,android studio的運行app里面,同時多了幾個可運行的專案,如下圖:

八、統一專案版本號
各個組件的build.gradle檔案中,有很多版本號,為了避免每次修改都得同時修改多份build.gradle檔案,也避免不同的組件使用的版本不一樣,導致沖突,所以我們可以把這些版本號統一管理起來,方法如下:
1、在專案根目錄下的build.gradle檔案中,定義版本號常量
ext {
/**
* 組件除錯模式
* isDebug = true 是組件模塊,說明是單獨的App
* isDebug = false是集成模式,說明是依賴Lib
* 每次更改“isDebug”的值后,需要點擊 "Sync Project" 按鈕
*
*/
isDebug = true
//版本號
android = [
compileSdkVersion: 30,
buildToolsVersion: "30.0.0",
minSdkVersion : 15,
targetSdkVersion : 30,
versionCode : 1,
versionName : "1.0"
]
}

2、然后在各個組件的build.gradle檔案中,做這樣的修改:
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
buildToolsVersion rootProject.ext.android.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
}
}

九、使用路由實作組件之間的通信
通過前面的學習,我們已經知道組件可以拆分了,但是當他們集成到main組件中時,還是需要一定的通信,例如業務A需要呼叫到業務B的一個頁面,甚至進行傳參,但是在“組件化”模式下,業務A和業務B是完全分開的,在業務A的認知里,根本就不存在業務B,也就沒辦法直接呼叫,當然,如果要業務A與業務B可以直接通信,互相引入就可以,但是這樣的話,專案耦合性太高,架構也混亂,會把“組件化”的所有優點都一一撇掉,所以我們應該用另外一種方式來處理,
這里需要引入一個概念:路由”,就如我們實際訪問網路一樣,我們電腦發送的請求都經過路由器轉發,在“組件化”中,我們也可以設定這么一個中轉站,來統一處理不同組件之間的呼叫關系,關于路由的用法,后面會補充,這里先用最簡單的方法,來實作組件之間的呼叫(頁面跳轉),代碼如下:
try {
Class c= Class.forName("net.zy13.module.modulea.MainActivity");
Intent intent = new Intent(context,c);
startActivity(intent);
} catch (ClassNotFoundException e) {
Log.e("組件","組件未集成,無法跳轉");
}
以上代碼是通過完整的類名來進行跳轉,在debug模式下,呼叫其他組件時,找不到對應組件,不會直接報錯,只是提示“未集成,無法跳轉”,我們可以把這個方法寫成一個工具類,放在common組件中,方法如下:
1、在common組件里創建一個工具類PageUtils.java,代碼如下:
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* @author 安陽 QQ:15577969
* @version 1.0
* @team 美奇軟體開發作業室
* @date 2020/11/12 11:55
*/
public class PageUtils {
/**
* 頁面跳轉
* @param context
* @param className
*/
public static void jump(Context context, String className){
try {
Class c = Class.forName(className);
Intent intent = new Intent(context,c);
context.startActivity(intent);
} catch (ClassNotFoundException e) {
Log.e("組件","未集成,無法跳轉");
}
}
/**
* 頁面跳轉,可以傳參,引數放在intent中,所以需要傳入一個intent
* @param context
* @param className
* @param intent
*/
public static void jump(Context context,String className,Intent intent){
try {
Class c = Class.forName(className);
intent.setClass(context,c);
context.startActivity(intent);
} catch (ClassNotFoundException e) {
Log.e("組件","未集成,無法跳轉");
}
}
}
2、在需要跳轉的地方,直接用以下方法呼叫:
PageUtils.jump(context,"net.zy13.module.modulea.MainActivity");
還有一種通信情況,就是其中某個組件的改變會影響到其他組件的改變,例如用戶的登陸情況(是否登錄),會影響到其他組件中一些控制元件的顯示情況,這個時候我們可以用android的廣播機制,或者用EventBus來解決,實際開發中,如果是像這種會影響全域的改變,應該放在公共的common組件之中,用戶的登陸情況,是影響全域的存在,那么就可以在common中,定義一個用戶單例,其他組件分別拿這個單例去控制其界面的顯示,特別是引入databingding之后,操作起來更為方便,具體的實作方式大家可以百度參考其他教程,這里我就不實作了,
十、android路由框架
關于路由框架的具體用法,我單獨整理成了一篇文章,示例代碼也是寫在了組件化專案里,
Android的路由框架用法:https://blog.csdn.net/qq15577969/article/details/109690111
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/226263.html
標籤:其他
上一篇:Activity的四種啟動模式
