首頁 > 軟體

Android元件化原理詳細介紹

2022-07-28 22:04:54

什麼是元件化?

一個大型APP版本一定會不斷的迭代,APP裡的功能也會隨之增加,專案的業務也會變的越來越複雜,這樣導致專案程式碼也變的越來越多,開發效率也會隨之下降。並且單一工程下程式碼耦合嚴重,每修改一處程式碼後都要重新編譯,非常耗時,單獨修改的一個模組無法單獨測試。

元件化架構的目的是讓各個業務變得相對獨立,各個元件在元件模式下可以獨立開發偵錯,整合模式下又可以整合到“app殼工程”中,從而得到一個具有完整功能的APP。

元件化每一個元件都可以是一個APP可以單獨修改偵錯,而不影響總專案。

為什麼使用元件化?

編譯速度: 可以但需測試單一模組,極大提高了開發速度
超級解耦: 極度降低了模組間的耦合,便於後期的維護和更新
功能重用: 某一塊的功能在另外的元件化專案中使用只需要單獨依賴這一模組即可
便於團隊開發: 元件化架構是團隊開發必然會選擇的一種開發方式,它能有效的使團隊更好的共同作業

一步步搭建元件化

這裡以演示為例,只設定登入這一個功能元件

元件化開發要注意的幾點問題 :

  • 要注意包名和資原始檔命名衝突問題
  • Gradle中的版本號的統一管理
  • 元件在AppIicationLibrary之間如何做到隨意切換
  • AndroidManifest. xml檔案的區分
  • Library不能在Gradle檔案中有applicationId

這裡以演示為例,只設定登入和個人中心這兩個功能元件

1.新建模組

並且在module裡新建一個activity

到這裡我們看到login和我們的app都在有一個綠點證明建立成功

個人中心member模組建立同理,並且每個模組目前都可以獨立執行。

2.統一Gradle版本號

每一個模組都是一個application,所以每個模組都會有一個build.gradle,各個模組裡面的設定不同,我們需要重新統一Gradle
在主模組建立config.gradle

config.gradle裡去新增一些版本號

ext{
    android = [
            compileSdkVersion :30,
            buildToolsVersion: "30.0.2",
            applicationId :"activitytest.com.example.moduletest",
            minSdkVersion: 29,
            targetSdkVersion :30,
            versionCode :1,
            versionName :"1.0",
    ]
    androidxDeps = [
            "appcompat": 'androidx.appcompat:appcompat:1.1.0',
            "material": 'com.google.android.material:material:1.1.0',
            "constaraintlayout": 'androidx.constraintlayout:constraintlayout:1.1.3',
    ]
    commonDeps = [
            "arouter_api"          : 'com.alibaba:arouter-api:1.5.1',
            "glide"                : 'com.github.bumptech.glide:glide:4.11.0'

    ]
    annotationDeps = [
            "arouter_compiler" : 'com.alibaba:arouter-compiler:1.5.1'
    ]
    retrofitDeps = [
            "retrofit"  : 'com.squareup.retrofit2:retrofit:2.9.0',
            "converter" : 'com.squareup.retrofit2:converter-gson:2.9.0',
            "rxjava"    : 'io.reactivex.rxjava2:rxjava:2.2.20',
            "rxandroid" : 'io.reactivex.rxjava2:rxandroid:2.1.1',
            "adapter"   : 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
    ]
    androidxLibs = androidxDeps.values()
    commonLibs = commonDeps.values()
    annotationLibs = annotationDeps.values()
    retrofitLibs = retrofitDeps.values()
}

在主模組的build.gradle裡新增

apply from: "config.gradle"

在各模組中去參照這些版本號
參照格式如下,兩種寫法均可

compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android.buildToolsVersion

參照前:

參照後:

並且使用同樣的方法,我們還可以統一我們的依賴庫在config.gradle裡去新增我們要依賴的庫,並在各個模組中去新增依賴

 implementation  rootProject.ext.dependencies.publicImplementation

也可以採用第二種寫法

dependencies = [

            "appcompat"             : 'androidx.appcompat:appcompat:1.2.0',

            "material"               : 'com.google.android.material:material:1.2.1',
            "constraintLayout"       : 'androidx.constraintlayout:constraintlayout:2.0.4',//約束性佈局

            //test
            "junit"                  : "junit:junit:4.13.1",
            "testExtJunit"           : 'androidx.test.ext:junit:1.1.2',//測試依賴,新建專案時會預設新增,一般不建議新增
            "espressoCore"           : 'androidx.test.espresso:espresso-core:3.3.0',//測試依賴,新建專案時會預設新增,一般不建議新增

    ]

新增依賴:

dependencies {

    implementation rootProject.ext.dependencies.appcompat
    implementation  rootProject.ext.dependencies["constraintLayout"]
    testImplementation rootProject.ext.dependencies["junit"]
    androidTestImplementation rootProject.ext.dependencies["testExtJunit"]
    androidTestImplementation rootProject.ext.dependencies["espressoCore"]

}

3.建立基礎庫

和新建module一樣,這裡需要新建一個library我們把它命名為Baselibs

同樣需要統一版本號,由於這是一個library模組,所以它不需要applicationId

我們一樣可以把它寫進config.gradle

other:[path:':Baselibs']

在每個模組去呼叫

implementation  project(rootProject.ext.dependencies.other)

同理,當本地庫為單獨所用,我們可以直接呼叫,而不需要將其寫入config.gradle,兩種方法選擇合適使用即可。

implementation project(':Baselibs')

但有時因為gradle版本問題,我們可能無法依賴到這些公共庫,因為我們在config.gradle裡是以陣列形式定義的,這時我們可以同for-each迴圈的方法將其依次匯入
config.gradle

dependencies = [
          ......
        other:[':Baselibs']
    ]

其他模組的build.gradle

dependencies {
......
    rootProject.ext.dependencies.other.each{
        implementation project(it)
    }

4.元件模式和整合模式轉換

在主模組gradle.properties裡新增布林型別選項。

在各個模組的build.gradle裡新增更改語句

if(is_Module.toBoolean()){
    apply plugin: 'com.android.application'
}else{
    apply plugin: 'com.android.library'
}

每個模組的applicationId也需要處理

if(is_Module.toBoolean()){
            applicationId "activitytest.com.example.login"
        }

當我們將is_module改為false時,再次執行編譯器我們的模組都不能單獨執行了

在app模組中新增判斷依賴就可以在整合模式下將各模組新增到app主模組中

// 每加入一個新的模組,就需要在下面對應的新增一行
    if (is_Module.toBoolean())]) {
        implementation project(path:':login')
        implementation project(path:':member')
    }

5.AndroidManifest的切換

為了單獨開發載入不同的AndroidManifest這裡需要重新區分下。
在元件模組裡的main檔案裡新建manifest資料夾

並且重寫一個AndroidManifest.xml檔案,整合模式下,業務元件的表單是絕對不能擁有自己的 Application 和 launch 的 Activity的,也不能宣告APP名稱、圖示等屬性,總之app殼工程有的屬性,業務元件都不能有,在這個表單中只宣告了應用的主題,而且這個主題還是跟app殼工程中的主題是一致的

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.login">

    <application
        android:theme="@style/Theme.MoudleTest">
        <activity android:name=".LoginActivity">
       
        </activity>
    </application>

</manifest>

並且我們還要使其在不同的模式下載入不同的AndroidManifest只需在各模組的build.gradle裡新增更改語句

sourceSets {
        main {
            if (is_Module.toBoolean()) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/mainfest/AndroidManifest.xml'
            }
        }
    }

6.*業務Application切換

每個模組在執行時都會有自己的application,而在元件化開發過程中,我們的主模組只能有一個application,但在單獨執行時又需要自己的application這裡就需要設定一下。
在業務模組新增新資料夾命名module

在裡面建一個application檔案

並且我們在build.gradle檔案裡設定module資料夾使其在單獨執行時能夠執行單獨的application
在設定manifest的語句中新增java.srcDir 'src/main/module'

sourceSets {
        main {
            if (is_Module.toBoolean()) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java.srcDir 'src/main/module'
            } else {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            }
        }
    }

同時我們在basic基礎層內新建application,用於載入一些資料的初始化

public class BaseApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Log.e("fff","baseapplication");
    }
}

在業務模組內module裡重寫該模組的application

public class LoginApplication extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }
}

至此,元件化框架搭建結束

元件之間的跳轉

這裡採用阿里巴巴的開源庫ARouter來實現跳轉功能,我會在以後的文章單獨拿出一篇來一步步去解讀Arouter原始碼,讓我們自己去搭建一個自己的路由

一個用於幫助 Android App 進行元件化改造的框架 —— 支援模組間的路由、通訊、解耦

由 github 上 ARouter 的介紹可以知道,它可以實現元件間的路由功能。路由是指從一個介面上收到封包,根據資料路由包的目的地址進行定向並轉發到另一個介面的過程。這裡可以體現出路由跳轉的特點,非常適合元件化解耦。

要使用 ARouter 進行介面跳轉,需要我們的元件對 Arouter 新增依賴,因為所有的元件都依賴了 Baselibs模組,所以我們在 Baselibs 模組中新增 ARouter 的依賴即可。其它元件共同依賴的庫也最好都放到 Baselibs中統一依賴。

這裡需要注意的是,arouter-compiler 的依賴需要所有使用到 ARouter 的模組和元件中都單獨新增,不然無法在 apt 中生成索引檔案,也就無法跳轉成功。並且在每一個使用到 ARouter 的模組和元件的 build.gradle 檔案中,其 android{} 中的 javaCompileOptions 中也需要新增特定設定。

1.新增依賴

Baselibs裡的build.gradle新增依賴

dependencies {
    api 'com.alibaba:arouter-api:1.3.1'
    // arouter-compiler 的註解依賴需要所有使用 ARouter 的 module 都新增依賴
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}
// 所有使用到 ARouter 的元件和模組的 build.gradle
android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
        }
    }
}
dependencies {
    ...
    implementation project (':base')
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}

主模組需要對跳轉模組進行依賴:

// 主專案的 build.gradle 需要新增對 login 元件和 share 元件的依賴
dependencies {
    // ... 其他
    implementation project(':login')
    implementation project(':share')
}

2.初始化ARouter

新增了對 ARouter 的依賴後,還需要在專案的 Application 中將 ARouter 初始化,我們這裡將 ARouter 的初始化工作放到主模組Application 的 onCreate()方法中,在應用啟動的同時將 ARouter 初始化。

public class MainApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化 ARouter
        if (isDebug()) {           
            // 這兩行必須寫在init之前,否則這些設定在init過程中將無效
            // 列印紀錄檔
            ARouter.openLog();     
            // 開啟偵錯模式(如果在InstantRun模式下執行,必須開啟偵錯模式!線上版本需要關閉,否則有安全風險)
            ARouter.openDebug();   
        }

        // 初始化 ARouter
        ARouter.init(this);

    }
    private boolean isDebug() {
        return BuildConfig.DEBUG;
    }
}

3.新增跳轉

這裡我們在首頁新增登入分享兩個跳轉頁面。

login.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ARouter.getInstance().build("/login/login").navigation();
    }
});
share.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ARouter.getInstance().build("/share/share").navigation();
    }
});

然後,需要在登入和分享元件中分別新增 LoginActivityShareActivity ,然後分別為兩個 Activity 新增註解 Route,其中path 是跳轉的路徑,這裡的路徑需要注意的是至少需要有兩級,/xx/xx

@Route(path = "/login/login")
public class Login extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

    }
}
@Route(path = "/share/share")
public class Share extends AppCompatActivity {

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

這樣就可以實現跳轉了。

元件之間的資料傳遞

由於主專案與元件,元件與元件之間都是不可以直接使用類的相互參照來進行資料傳遞的,那麼在開發過程中如果有元件間的資料傳遞時應該如何解決呢,這裡我們可以採用 [介面 + 實現] 的方式來解決。

Baselibs基礎庫裡定義元件可以對外提供存取自身資料的抽象方法的 Service。並且提供了一個 ServiceFactory,每個元件中都要提供一個類實現自己對應的 Service 中的抽象方法。在元件載入後,需要建立一個實現類的物件,然後將實現了 Service 的類的物件新增到ServiceFactory 中。這樣在不同元件互動時就可以通過 ServiceFactory 獲取想要呼叫的元件的介面實現,然後呼叫其中的特定方法就可以實現元件間的資料傳遞與方法呼叫。

當然,ServiceFactory 中也會提供所有的 Service 的空實現,在元件單獨偵錯或部分整合偵錯時避免出現由於實現類物件為空引起的空指標異常。

下面我們就按照這個方法來解決元件間資料傳遞與方法的相互呼叫這個問題,這裡我們通過分享元件 中呼叫 登入元件 中的方法來獲取登入狀態是否登入這個場景來演示。

1.定義介面

其中 service資料夾中定義介面,LoginService 介面中定義了 Login 元件向外提供的資料傳遞的介面方法,EmptyService 中是 service 中定義的介面的空實現,ServiceFactory 接收元件中實現的介面物件的註冊以及向外提供特定元件的介面實現。

LoginService

public interface LoginService {
    /**
     * 是否已經登入
     * @return
     */
    boolean isLogin();
    /**
     * 獲取登入使用者的 Password
     * @return
     */
    String getPassword();
}

EmptyService

public class EmptyService implements LoginService {
    @Override
    public boolean isLogin() {
        return false;
    }

    @Override
    public String getPassword() {
        return null;
    }
}

ServiceFactory

public class ServiceFactory {
    private LoginService loginService;
    private ServiceFactory(){
 /**
     * 禁止外部建立 ServiceFactory 物件
     */
    private ServiceFactory() {
    }
    /**
     * 通過靜態內部類方式實現 ServiceFactory 的單例
     */
    public static ServiceFactory getInstance() {
        return Inner.serviceFactory;
    }
    private static class Inner {
        private static ServiceFactory serviceFactory = new ServiceFactory();
    }
 /**
     * 接收 Login 元件實現的 Service 範例
     */
    public void setLoginService(LoginService loginService){
        this.loginService = loginService;
    }
       /**
     * 返回 Login 元件的 Service 範例
     */
    public LoginService getLoginService(){
        if(loginService == null){
            return new EmptyService();
        }else{
            return loginService;
        }
    }
}

2.實現介面

在login模組

public class AccountService implements LoginService {

    private boolean login;
    private String password;

    public AccountService(boolean login, String password) {
        this.login = login;
        this.password = password;
    }

    @Override
    public boolean isLogin() {
        return login;
    }

    @Override
    public String getPassword() {
        return password;
    }
}

這裡新建一個Util類用來儲存登入資料

public class LoginUtil {
    static boolean isLogin = false;
    static String password = null;
}

實現一下登入操作

login = (Button)findViewById(R.id.login);
        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoginUtil.isLogin = true;
                LoginUtil.password = "admin";
                ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password));
            }
        });

在login模組的application裡定義ServiceFactory類

public class LoginApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password));
    }
}

在分享模組獲取登入資訊

share = (Button)findViewById(R.id.share);
        share.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(ServiceFactory.getInstance().getLoginService().isLogin()){
                    Toast.makeText(ShareActivity.this,"分享成功!",Toast.LENGTH_SHORT).show();
                }else{
                    Toast.makeText(ShareActivity.this,"分享失敗,請先登入!",Toast.LENGTH_SHORT).show();
                }
            }
        });

一個專案時只能有一個 Application 的,Login 作為元件時,主模組的 Application 類會初始化,而 Login 元件中的 Applicaiton 不會初始化。確實是存在這個問題的,我們這裡先將 Service 的註冊放到其活動裡,稍後我們會解決 Login 作為元件時 Appliaciton 不會初始化的問題。

元件Application的動態切換

在主模組中有 Application 等情況下,元件在集中偵錯時其 Applicaiton 不會初始化的問題。而我們元件的 Service 在 ServiceFactory 的註冊又必須放到元件初始化的地方。

為了解決這個問題可以將元件的 Service 類強參照到主 Module 的 Application 中進行初始化,這就必須要求主模組可以直接存取元件中的類。而我們又不想在開發過程中主模組能存取元件中的類,這裡可以通過反射來實現元件 Application 的初始化。

1.定義抽象類 BaseApplication 繼承 Application

Baselibs基礎庫模組

public abstract class BaseApplication extends Application {
    /**
     * Application 初始化
     */
    public abstract void initModuleApp(Application application);

    /**
     * 所有 Application 初始化後的自定義操作
     */
    public abstract void initModuleData(Application application);              //其他需要呼叫的方法
}

2.所有的元件的 Application 都繼承 BaseApplication

這裡我們以Login模組為例

public class LoginApplication extends BaseApplication{
    @Override
    public void onCreate() {
        super.onCreate();
        initModuleApp(this);
        initModuleData(this);
    }
    @Override
    public void initModuleApp(Application application) {
        ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password));
    }
    @Override
    public void initModuleData(Application application) {

    }
}

3.定義 AppConfig 類

Baselibs模組定義一個靜態的 String 陣列,我們將需要初始化的元件的 Application 的完整類名放入到這個陣列中。

public class AppConfig {
    private static final String LoginApp = "com.example.login.LoginApplication";
    public static String[] moduleApps = {
            LoginApp
    };
}

4.主模組application實現兩個初始化方法

// 主 Module 的 Applicaiton
public class MainApplication extends BaseApp {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // 初始化元件 Application
        initModuleApp(this);
        
        // 其他操作
        
        // 所有 Application 初始化後的操作
        initModuleData(this);
        
    }

    @Override
    public void initModuleApp(Application application) {
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                BaseApp baseApp = (BaseApp) clazz.newInstance();
                baseApp.initModuleApp(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    public void initModuleData(Application application) {
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                BaseApp baseApp = (BaseApp) clazz.newInstance();
                baseApp.initModuleData(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
}

到這裡我們就通過反射,完成了元件 Application 的初始化操作,也實現了元件與化中的解耦需求。

主模組使用其他元件的 Fragment

我們在開發過程中經常使用 Fragment。一般情況下,我們都是直接通過存取具體 Fragment 類的方式實現 Fragment 的範例化,但是現在為了實現模組與元件間的解耦,在移除元件時不會由於參照的 Fragment 不存在而編譯失敗,我們就不能模組中直接存取元件的 Fragment 類。
這裡介紹兩種方法

1.ARouter

這裡可以採用ARouter直接呼叫

fragment = (Fragment) ARouter.getInstance().build("/login/fragment").navigation();

2.反射

我們還是以Login模組為例,假如在該模組建立一個使用者介面,命名為UserFragment
首先,在 Login元件中建立 UserFragment,然後在 LoginService 介面中新增newUserFragment方法返回一個Fragment,在Login元件中的 AccountServiceBaselibsLoginService 的空實現類中實現這個方法,然後在主模組中通過 ServiceFactory 獲取 LoginService 的實現類物件,呼叫其 newUserFragment 即可獲取到 UserFragment 的範例。

// Baselibs 模組的 LoginService 
public interface LoginService {
//其他程式碼...
    Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag);
}
// Login 元件中的 AccountService
public class AccountService implements LoginService {
    // 其他程式碼 ...

    @Override
    public Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag) {
        FragmentTransaction transaction = manager.beginTransaction();
        // 建立 UserFragment 範例,並新增到 Activity 中
        Fragment userFragment = new UserFragment();
        transaction.add(containerId, userFragment, tag);
        transaction.commit();
        return userFragment;
    }
}
// 主模組的 FragmentActivity
public class FragmentActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fragment);
        
        // 通過元件提供的 Service 實現 Fragment 的範例化
        ServiceFactory.getInstance().getAccountService().newUserFragment(this, R.id.layout_fragment, getSupportFragmentManager(), null, "");
    }
}

到此這篇關於Android元件化原理詳細介紹的文章就介紹到這了,更多相關Android元件化 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com