Android元件化搭建分享

沈敏傑發表於2017-12-16

1.元件化開發

元件化開發這個名詞並不陌生,但真正什麼才是元件化開發,大家在網上搜可以檢視很多相應的文章,我概念中,模組化的開發,就是把很多模組獨立出來,基礎模組,業務模組等。什麼是基礎模組,基礎模組就是公司常用的一些sdk,一些封裝好的基類,業務模組就是基於基礎模組進行開發的。在以往的開發中,我並未真正的去使用元件化開發,直到加入新的團隊可以說是開啟新世界的大門,給我的感覺,元件化開發,賊爽,為什麼爽?

我總結了好幾點:

1.各自負責業務模組獨立開發,以application進行開發,後期再以library引入專案 2.因為每個模組獨立出來,以最小模組的進行開發,編譯速度快 3.有利於單元測試,對業務模組進行單元測試 4.降低耦合,提高模組的複用

以下為基礎模組包:

package.png

整個專案結構:

Android框架模組分佈圖.png

Android studio:

圖片2.png

在gradle.properties中,我們可以設定一個變數,控制是否使用模組化來開發

#是否使用模組化開發
isModule=false
複製程式碼

然後在settings.gradle中設定專案引入包

//預設都開啟基礎模組
include ':sdk', ':model', ':widget', ':module-basic'
//根據自己負責的模組分別進行相應的引入
include ':module-user'
include ':module-business'
//根據是否模組開發,是否引入app 模組
if (!isModule.toBoolean()) {
    include ':app'
}
複製程式碼

業務模組gradle進行模組判斷

圖片3.png

//通過之前設定的變數進行設定是application還是library
if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
複製程式碼

根據結構圖,我們基礎模組的依賴,預設引入sdk、model、widget、module-baisc 然後根據自己負責的業務模組,分別引入不同的業務,如果我是負責使用者模組,我在開發就只需要引入使用者模組即可,這樣開發每個模組的時候可以提高每個模組的編譯效率。

最後模組合併的時候,在gradle.properties中關閉模組開發,在settings.gradle引入專案相應的模組包,並設定app的build-gradle:

圖片4.png

build-gradle:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.android.support:design:26.+'
    testCompile 'junit:junit:4.12'

    //如果不使用模組化開發,就引入所有的業務模組
    if (!isModule.toBoolean()) {
        compile project(':module-business')
        compile project(':module-user')
    }
}
複製程式碼

現在的問題,不同模組的activity怎麼跳轉,以前我的做法都會在每個activity中寫一個靜態方法,把入參設定好.

/**
 * 跳轉
 *
 * @param context 上下文
 * @param param   引數
 */
public static void toAcitivty(Context context, String param) {
    Intent intent = new Intent(context, MainActivity.class);
    intent.putExtra("param", param);
    context.startActivity(intent);
}
複製程式碼

因為使用模組化開發的話,不同業務模組是不能呼叫其activity,因此我們使用阿里的Arouter, 在每個activity頭部使用註解進行跳轉,就像Spring mvc 的controller一樣,使用路由進行設定跳轉,在模組化的開發中,這個很關鍵,一方面使用arouter可以降低activity之間的耦合,另一方面可以對模組進行單元測試。

Arouter具體的使用方法: github.com/alibaba/ARo…

2.Retrofit+RxJava+MVP模式

關於Retrofit跟RxJava,具體詳細的用法就不在這裡介紹,網上有很多現有的文章,為什麼使用Retrofit跟RxJava,Retrofit是基於Okhttp封裝一層的客戶端,配合RxJava執行緒排程,很好的控制網路請求,使用RxJava可以提高程式碼的可讀性,這裡我分享一下retrofit+Rxjava封裝。

1.基於Retrofit的Api工廠

ApiFactory如下圖:

api工廠.png

圖中的ApiFactory的職責是提供所有業務Api介面,具體提供的Api是通過介面ApiProvider提供每個業務介面,如果使用者介面,交易介面,令牌介面等,ApiFactory通過單例,獲取api提供者,ApiProvider具體的實現類ApiProvideImpl繼承於網路引擎RetrofitApi,RetrofitApi用於初始化一些網路引擎。ApiProvideImpl中使用retrofit來初始化各種Api介面。

ApiProviderImpl.java:

class ApiProviderImpl extends RetrofitApi implements ApiProvider {

    private OkHttpClient httpClient;

    ApiProviderImpl(Context applicationContext) {
        super(applicationContext);
    }

    private <T> T create(Class<T> cls) {
        return mRetrofit.create(cls);
    }


    @Override
    public ITokenApi getTokenApi() {
        return create(ITokenApi.class);
    }

    @Override
    public IUserApi getUserApi() {
        return create(IUserApi.class);
    }

    @Override
    public IProductApi getProductApi() {
        return create(IProductApi.class);
    }

    @Override
    public IPushApi getPushApi() {
        return create(IPushApi.class);
    }

    @Override
    public IQuotationApi getQuotationApi() {
        return create(IQuotationApi.class);
    }

    @Override
    public ITradeApi getTradeApi() {
        return create(ITradeApi.class);
    }

    .....
}
複製程式碼

2.MVP

使用mvp可以解耦,結構清晰,對於業務複雜的場景來說,可以提高程式碼可讀性,結構清晰,降低後期維護成本。如下圖登入模組所示:

mvp.png

View跟presenter都抽象成介面,這樣相互不依賴於細節,有易於做單元測試,降低耦合。這裡有兩個基礎介面,LoginView跟LoginPresenter分別繼承於IView跟IPresenter,LoginViewImpl以及LoginPresenterImpl分別實現LoginView跟LoginPresenter,其依賴於抽象不依賴於實現的細節。

/**
 * 登入契約類
 */
public interface LoginContract {

    /**
     * 表現層介面
     */
    interface Presenter extends IPresenter {

        /**
         * 登入操作
         */
        void login();
    }

    /**
     * 檢視層介面
     */
    interface View extends IPresenterView {

        /**
         * 獲取密碼
         *
         * @return return
         */
        String getPassword();

        /**
         * 獲取使用者資訊
         *
         * @return return
         */
        String getUsername();

        /**
         * 登入成功
         */
        void loginSuccess();

        /**
         * 登入失敗
         *
         * @param msg msg
         */
        void loginFailed(String msg);
    }
}

複製程式碼

我們通過定義一個Contract契約類,來制定介面,在定Presenter跟view介面的同時,我們可以很清晰的知道,表現層需要什麼東西,view層需要提供什麼東西,包括網路請求後相應的響應,這樣在我們做一個業務邏輯的時候思路可以更清晰,同事在進行presenter複用以及單元測試會更方便。

3.結合Retrofit+RxJava+Mvp

結合之前談到的Api跟mvp,在這個基礎上進行封裝Presenter的實現基礎類。

/**
 * presenter基礎實現類的封裝
 * 1.跟檢視view進行繫結與解綁
 * 2.對rx事件進行加工處理與釋放資源
 */
public class BasicPresenterImpl<T extends IPresenterView> implements IPresenter {

    /**
     * 檢視
     */
    protected T mView;

    /**
     * 上下文
     */
    protected Context mContext;

    /**
     * 記錄標識,用於此presenter所有的任務進行標識
     */
    private String mTag = this.getClass().getName();

    public BasicPresenterImpl(Context context, T view) {
        this.mView = view;
        this.mContext = context;
    }

    public void start() {
    }

    /**
     * 銷燬資源,一般用於與view解綁操作
     * 如activity作為view中,activity 銷燬的時候呼叫
     * 避免錯誤引用,避免記憶體洩露
     */
    public void destroy() {
        this.mView = null;
        this.mContext = null;
        this.cancelRequest();
    }

    /**
     * 根據tag清掉任務,如清掉未完成的網路請求
     */
    protected void cancelRequest() {
        RxObservable.dispose(this.mTag);
        RxObservable.dispose("PageDataObservable");
    }


    /**
     * rxJava  多數用於建立網路請求
     * 如createObservable(mUser.login())
     * retorfit結合rxJava
     *
     * @param observable observable
     * @param <T>        t
     * @return return
     */
    protected <T> Observable<T> createObservable(Observable<T> observable) {
        //建立任務
        return RxObservable.create(observable, this.mTag);
    }
}
複製程式碼

基礎Presenter封裝了繫結與解綁的操作,presenter跟view解綁時呼叫destory釋放資源,並把此presenter中使用rxJava處理得事件全部清掉,釋放資源,例如一些網路請求,當view跟presenter解綁後網路請求未來得及返回處理,容易出現view空指標的操作。

接著介紹一下RxObservable的封裝:

/**
 * 用於封裝rx事件,通過鍵值對的方式儲存任務
 * 對任務進行初始化,釋放等操作所
 */
public final class RxObservable {

    /**
     * 全域性變數,使用tag標識儲存Disposable集合
     * Disposable?Observer(觀察者)與Observable(被觀察者)切斷連結
     */
    private static final Map<String, List<Disposable>> sObservableDisposableList = new WeakHashMap();

    public RxObservable() {
    }

    /**
     * 建立被觀察者,如retrofit集合rxJava返回的網路請求,
     * 此方法用於事件在初始化時進行處理,把此事件儲存到sObservableDisposableList集合中,
     * 以tag為key,以為List<Disposable>為值,訂閱被觀察者時可以獲其Disposable
     */
    public static <T> Observable<T> create(Observable<T> observable, final String tag) {
        return observable.doOnSubscribe(new Consumer() {
            public void accept(@NonNull Disposable disposable) throws Exception {
                //在集合中判斷是否存在集合
                //沒有則建立,並以key-tag儲存到sObservableDisposableList中
                List disposables = (List) RxObservable.sObservableDisposableList.get(tag);
                if (disposables == null) {
                    ArrayList disposables1 = new ArrayList();
                    RxObservable.sObservableDisposableList.put(tag, disposables1);
                }
                //把此事件的Disposable新增到對應的tag的集合中
                ((List) RxObservable.sObservableDisposableList.get(tag)).add(disposable);
            }
            //訂閱過程在Io執行緒處理,傳送在主執行緒處理
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    /**
     * 釋放所有資源
     */
    public static void dispose() {
        try {
            Iterator e = sObservableDisposableList.values().iterator();
            while (e.hasNext()) {
                List disposables = (List) e.next();
                Iterator var2 = disposables.iterator();

                while (var2.hasNext()) {
                    Disposable disposable = (Disposable) var2.next();
                    if (disposable != null && !disposable.isDisposed()) {
                        disposable.dispose();
                    }
                }

                disposables.clear();
            }
        } catch (Exception var7) {
            Log.e("rae", "釋放HTTP請求失敗!", var7);
        } finally {
            sObservableDisposableList.clear();
        }

    }

    /**
     * 根據tag標識進行釋放資源
     *
     * @param tag tag
     */
    public static void dispose(String tag) {
        try {
            if (!TextUtils.isEmpty(tag) && sObservableDisposableList.containsKey(tag)) {
                List e = (List) sObservableDisposableList.get(tag);
                Iterator var2 = e.iterator();

                while (var2.hasNext()) {
                    Disposable disposable = (Disposable) var2.next();
                    if (disposable != null && !disposable.isDisposed()) {
                        disposable.dispose();
                    }
                }

                e.clear();
                sObservableDisposableList.remove(tag);
                return;
            }
        } catch (Exception var7) {
            Log.e("rae", "釋放HTTP請求失敗!", var7);
            return;
        } finally {
            sObservableDisposableList.remove(tag);
        }

    }
}
複製程式碼

在RxObservable中,建立一個sObservableDisposableList用於儲存每個presenter中處理的事件,通過tag作為標識建立,每個presenter中會通過tag找到對應的Disposable集合,Disposable集合中儲存了此presenter中的所有任務,如網路請求、io操作等,通過此方法可以統一管理tag的任務,在presenter解綁的時候可以及時的銷燬資源,避免記憶體洩露。

登入的一個小例子:

public class LoginPresenterImpl extends BasicPresenterImpl<LoginContract.View> implements LoginContract.Presenter {

    IUserApi mUserApi;

    public LoginPresenterImpl(Context context, LoginContract.View view) {
        super(context, view);
        //初始化變數....
    }

    @Override
    public void login() {
        //在view層獲取手機號跟密碼
        final String mobile = mView.getMobile();
        final String password = mView.getPassword();
        if (TextUtils.isEmpty(mobile)) {
            mView.onLoginFailed("請輸入手機號碼");
            return;
        }
        if (TextUtils.isEmpty(password)) {
            mView.onLoginFailed("請輸入密碼");
            return;
        }
        if (!mPhoneValidator.isMobile(mobile)) {
            mView.onLoginFailed("請輸入正確的手機號碼");
            return;
        }
        createObservable(mUserApi.login(mobile, password)).subscribe(new ApiDefaultObserver<UserInfo>() {
            @Override
            protected void onError(String msg) {
                //登入失敗
                mView.onLoginFailed(msg);
            }

            @Override
            protected void accept(UserInfo userInfo) {
                //登入成功等操作
            }
        });
    }

    
}
複製程式碼

相關文章