android使用主流庫搭建應用框架

通用庫


在開發android應用時,一般會使用一些現有庫來縮短開發週期,將代碼進行模塊化;
使用框架雖然可能會增加最終代碼量,但在開發過程中會非常方便。

項目模版:GITHUB地址

android從出生到現在已經很多年了,因此有大量的庫可供使用,android使用的技能不會特別多,但比較雜,雖然所有的代碼都可以通過手✖完成,但這個過程肯定會相當痛苦。

很多庫使用率都很高,通過github上的star數量可以分辨出哪些庫比較主流,通過更新頻率也可以查看哪些庫依然在維護。現整理一些比較好的庫,同時通過這些項目來搭建出一般android項目可以直接通用的框架代碼。

一、框架需設計的庫

雖然說一般框架不需要太多的內容,不過要實現大部分的功能,還是需要引用較多的資源庫的,這裏進行列舉,然後接下來進行簡單的說明:

  1. 基本通用包(系統自帶或者support庫提供):
    • multidex:dex分包處理
    • constraint
    • palette:着色
    • cardview:卡片佈局
    • design
  2. 數據庫
    • litepal
  3. 自動注入/變量賦值框架:
    • dagger:爲成員變量等自動賦值
    • butterknife:依據xml自動生成變量並賦值,點擊監聽
  4. 線程任務處理
    • Rxjava(RxAndroid)
  5. 網絡任務:
    • retrofit
    • okhttp:http核心庫
  6. 數據類型轉換:
    • Gson:json數據處理
  7. 圖片處理:
    • Glide:圖片下載裁剪
    • zxing-library:二維碼圖片識別或生成
    • takephoto:從設備中選擇圖片或拍照獲得圖片
  8. 工具類:
    • utilcode:這是工具庫集合,提供各種常用的操作處理
    • statusbarutil:狀態欄工具類
  9. 調試時檢測優化:
    • leakcanary-android
  10. 日誌工具
    • Logger:打印日誌
  11. 彈出框:
    • pickerview:時間選擇器、條件選擇器
  12. 權限處理:
    • permissionsdispatcher:無反射動態請求權限
  13. 自定義view:
    • flowlayout:tag組
    • xrecyclerview:可上拉加載下拉刷新的recyclerview
    • convenientbanner:輪播插件
    • BottomNavigationViewEx:底部導航欄(多tab視圖)
    • roundedimageview:圓角頭像,可存在border
    • gridPasswordView:密碼框
    • badge:角標庫
  14. 界面跳轉:
    • ARouter:阿里推出的庫,可實現路由跳轉、攔擊、降級、參數自動注入
  15. 其他:
    • pushsdk:友盟統計、消息推送(module形式)
    • rxdownload:基於Rxjava的用於軟件更新的庫
    • logging-interceptor:網絡請求時打印信息
    • adapter-rxjava:將網絡請求變爲Rxjava監聽形式
    • converter-gson:網絡返回數據(若爲gson格式)轉爲bean

大部分的庫都已經列舉出來了;
其中一些庫如自定義View等內容可以參考github源碼,也可以在項目中找到對應使用的位置
對於友盟推送等內容,也已經在代碼中集成;同時在BaseApplication類中進行了初始化
動態權限庫自身在AndroidStudio中是有插件的,並且源碼中啓動Activity就是示例;
LocationPickerView也提供了自定義View的模版。

其中有些庫功能很強大,但使用起來會進行其他配置,因此單獨說明:

二、網絡請求模塊

該模塊由Rxjava、RxAndroid、logging-interceptor、adapter-rxjava、converter-gson、retrofit、okhttp等項目包組成,將網絡請求、數據轉換、異常處理、日誌輸出等功能內聚到一起,整個架構可用簡圖概括(只是草圖):

這裏寫圖片描述

這樣用戶只需要將網絡請求的字段傳入,並定義可以處理的結構以及異常情況下的處理,就可以省略中間的具體的過程。

具體實現如下:

@Provides
@Singleton
HttpInterface provideHttpInterface() {
    //網絡請求的Host
    String baseUrl = BaseApplication.app.getBaseNetUrl();

    //生成JSON轉換的庫
    Gson gson = new GsonBuilder()
            .serializeNulls()
            .setDateFormat("yyyy:MM:dd HH:mm:ss")
            .create();
    GsonConverterFactory gsonConverterFactory = GsonConverterFactory.create(gson);

    //生成RxJava轉換的adapter
    RxJava2CallAdapterFactory rxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create();

    //生成OkHttp網絡傳輸的客戶端
    HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
    OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .cookieJar(new CookieJar() {
                @Override
                public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                    cookieStore.put(url.host(), cookies);
                }

                @Override
                public List<Cookie> loadForRequest(HttpUrl url) {
                    List<Cookie> cookies = cookieStore.get(url.host());
                    return cookies != null ? cookies : new ArrayList<>();
                }
            })
            .addInterceptor(chain -> {
                Request request = chain.request()
                        .newBuilder()
                        .addHeader("SDK", String.valueOf(Build.VERSION.SDK_INT ))
                        .build();
                return chain.proceed(request);
            })
            .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
            .addNetworkInterceptor(new StethoInterceptor())
            .connectTimeout(2000, TimeUnit.MILLISECONDS)
            .readTimeout(2000, TimeUnit.MILLISECONDS)
            .writeTimeout(2000,TimeUnit.MILLISECONDS)
            .build();

    //最後組合成Retrofit對象
    Retrofit retrofit = new Retrofit.Builder()
            .addConverterFactory(gsonConverterFactory)
            .addCallAdapterFactory(rxJava2CallAdapterFactory)
            .baseUrl(baseUrl)
            .client(okHttpClient)
            .build();

    //將註解後的interface請求接口轉換爲真正可用的網絡請求對象
    return retrofit.create(HttpInterface.class);
}

可以看到,最後其實是生成了HttpInterface對象。這個HttpInterface對象是自定義的,自身是接口類型,用於定義網絡請求所需的參數,以及可以處理的返回數據的類型,如假如需要請求一次網絡:

public interface HttpInterface {
    /**
     * 登錄
     *
     * @param username 用戶名
     * @param pwd      密碼
     * @return 登錄返回對象
     */
    @POST("login/doLogin")
    @FormUrlEncoded
    Observable<BaseHttpBean<Object>> doLogin(@Field("username") String username,
                                             @Field("pwd") String pwd);                                           
}

如上一般,只需要定義參數,以及可解析的數據對象即可。

然後發起網絡請求:

/**
 * 舉例請求網絡數據
 */
@OnClick(R.id.fab)
public void onViewClicked() {
    httpInterface.doLogin("name", "password")
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<BaseHttpBean<Object>>() {
                @Override
                public void accept(BaseHttpBean<Object> objectBaseHttpBean) throws Exception {
                    // TODO: 2018/3/10 成功 
                }
            }, new Consumer<Throwable>() {
                @Override
                public void accept(Throwable throwable) throws Exception {
                    // TODO: 2018/3/10 失敗 
                }
            });
}

網絡請求的超時判斷、數據 轉換等操作,觀察者方法執行時,就已經完成,如果需要添加其他的網絡任務,只需要在HttpInterface接口中添加對應的方法即可。

三、自動注入模塊

通過dagger可以把已經註解的成員變量進行賦值,只要提供對應類型的提供方即可;
利用dagger框架也可以很方便的實現MVP模式,同時可以省略View層與Presenter層的賦值操作,只要有需要對應變量的地方,直接通過@Inject註解即可。

Butterknife則專注於 對xml佈局文件中,變量的生成,以及控件的點擊監聽,同時ButterKnife還在Androidstudio上實現了plugin,使用起來比較方便。直接百度即可,這裏不額外說明;

以剛纔的網絡請求模塊,可能需要在多處調用HttpInterface的實例,這時就可以 通過dagger來完成這一功能:

1、爲dagger定義變量作用範圍

一般按照模版,定義兩個註解即可:

定義Activity作用範圍:

/**
 * 功能----定義每個activity的生命週期,供dagger框架使用
 * <p>
 * Created by MNLIN on 2017/9/22.
 */
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {
}

定義Fragment作用範圍:

/**
 * 功能----fragment對應dagger的生命週期控制
 * <p>
 * Created by MNLIN on 2017/9/23.
 */
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface PerFragment {

}

2、定義作用範圍內需要的變量,或全局變量

Activity和Fragment作用範圍內的變量是局部的;
Application作用範圍是全局的(Activity和Fragment需設置依賴Application的compont);
這點是通過compont組件來保證的;

這裏只寫出全局模式變量的注入方法:

/**
 * 功能----應用的組件
 * <p>
 * Created by MNLIN on 2017/9/22.
 */
@Singleton
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
    void inject(BaseApplication application);

    HttpInterface initHttpInterface();
}

可以看到,在ApplicationComponent中聲明瞭HttpInterface,這樣所有的位置只要通過@Inject進行註解,就可以獲取對應實例。

而該實例的創建則是通過module提供的,這點對於Activity,Fragment,Application相同:

/**
 * 功能----Application的module,爲ApplicationComponent提供對象生成器
 * Created by MNLIN on 2017/9/22
 */
@Singleton
@Module
public class ApplicationModule {
    String tag = "";
    private BaseApplication application;

    public ApplicationModule(BaseApplication application) {
        this.application = application;
    }

    @Provides
    @Singleton
    HttpInterface provideHttpInterface() {
        //網絡請求的Host
        String baseUrl = BaseApplication.app.getBaseNetUrl();

        //生成JSON轉換的庫
        Gson gson = new GsonBuilder()
                .serializeNulls()
                .setDateFormat("yyyy:MM:dd HH:mm:ss")
                .create();
        GsonConverterFactory gsonConverterFactory = GsonConverterFactory.create(gson);

        //生成RxJava轉換的adapter
        RxJava2CallAdapterFactory rxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create();

        //生成OkHttp網絡傳輸的客戶端
        HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cookieJar(new CookieJar() {
                    @Override
                    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                        cookieStore.put(url.host(), cookies);
                    }

                    @Override
                    public List<Cookie> loadForRequest(HttpUrl url) {
                        List<Cookie> cookies = cookieStore.get(url.host());
                        return cookies != null ? cookies : new ArrayList<>();
                    }
                })
                .addInterceptor(chain -> {
                    Request request = chain.request()
                            .newBuilder()
                            .addHeader("SDK", String.valueOf(Build.VERSION.SDK_INT ))
                            .build();
                    return chain.proceed(request);
                })
                .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
                .addNetworkInterceptor(new StethoInterceptor())
                .connectTimeout(2000, TimeUnit.MILLISECONDS)
                .readTimeout(2000, TimeUnit.MILLISECONDS)
                .writeTimeout(2000,TimeUnit.MILLISECONDS)
                .build();

        //最後組合成Retrofit對象
        Retrofit retrofit = new Retrofit.Builder()
                .addConverterFactory(gsonConverterFactory)
                .addCallAdapterFactory(rxJava2CallAdapterFactory)
                .baseUrl(baseUrl)
                .client(okHttpClient)
                .build();

        //將註解後的interface請求接口轉換爲真正可用的網絡請求對象
        return retrofit.create(HttpInterface.class);
    }
}

通過這樣一個流程,就可以完成前期設定工作,當然還需要最後一步:

3、在基類中初始化

之前只是進行了準備,但是真正注入還是需要在代碼執行時候來啓動,因此在BaseActivity、BaseFragment、BaseApplication中需要添加初始化代碼:

這裏只顯示Application部分:BaseApplication類

//注入dagger框架
applicationComponent = DaggerApplicationComponent.builder().applicationModule(new ApplicationModule(this)).build();
applicationComponent.inject(this);

具體詳細設置可以參考源碼;

四、跳轉攔截模塊——ARouter

這個模塊雖然基本功能看上去和平時使用startActivity的方式差不了多少,但是真正帶入後會發現很多精巧的地方,表面上看起來是提供路由跳轉的其他一種方式,只是高端一些,不過真正使用起來會發現出奇的好。

1、定義字符串,供路由跳轉

一般來說service使用的情況比ActivityFragment要少的多,因此如果需要使用service的話,可以前往ARouter的GITHUB進行查看。

ARouter框架提供了:路由跳轉,碎片生成,降級,自定義序列化等服務;不過用的雖然不多,但比較精巧的則是權限判斷

引用ARouter自己的說明:

6.爲目標頁面聲明更多信息

// 我們經常需要在目標頁面中配置一些屬性,比方說"是否需要登陸"之類的
// 可以通過 Route 註解中的 extras 屬性進行擴展,這個屬性是一個 int值,換句話說,單個int有4字節,也就是32位,可以配置32個開關
// 剩下的可以自行發揮,通過字節操作可以標識32個開關,通過開關標記目標頁面的一些屬性,在攔截器中可以拿到這個標記進行業務邏輯判斷
@Route(path = "/test/activity", extras = Consts.XXXX)

具體功能可以查看項目代碼:
以下給出引導部分:

/**
 * 功能----路徑跳轉activity/fragment
 * 
 * <p>
 * Created by MNLIN on 2017/11/24.
 */

public final class ARouterConst {
    /**
     * 無權限
     * 登錄
     * 綠色通道(若設定則無法跳轉,相當於禁止功能)
     * activity啓動:清除任務棧
     */
    public static final int FLAG_NONE = 0x00000000;
    public static final int FLAG_LOGIN = 0x00000003;
    public static final int FLAG_FORCE_ACCESS = 0x00000040;
    public static final int FLAG_ACTIVITY_CLEAR_TOP = 0x00000200;

    /**
     * activity/fragment
     */
    public static final String Activity_SelectFunctionActivity = "/activity/SelectFunctionActivity";
    public static final String Activity_SearchFilterActivity = "/activity/SearchFilterActivity";
    public static final String Fragment_WalletFragment = "/fragment/WalletFragment";
}

這裏下方以Activity_或者Fragment_開頭的常量提供ARouter路由進行最基本的功能跳轉,而上方則定義了項目中可能會用到的“權限”

假設現在有一場景,活動A向活動B進行跳轉,此時要求用戶已經登錄,否則的話就需要中斷跳向B的操作,直接跳轉到活動C;
一般情況下可能我們會在B中進行判斷,若是未登錄的話,則主動將頁面路由到C,可是這樣的話,實際並沒有阻止跳轉操作,若是C中一開始就進行危險操作,則可能會直接崩潰。
或者說可以在A中進行判斷,但如此一來,若是向B跳轉的活動過多,就需要在多個活動中編寫同樣的代碼,當然這可以編寫工具類,但若是頁面過多,這種邏輯會很麻煩.

而ARouter則相當於在跳轉過程中進行攔截,所有不符合條件的跳轉都將另行處理:

/**
 * function : 跳轉攔截器(權限攔截)
 * <p>
 * 比較經典的應用就是在跳轉過程中處理登陸事件,這樣就不需要在目標頁重複做登陸檢查
 * 攔截器會在跳轉之間執行,多個攔截器會按優先級順序依次執行
 *
 * @author MNLIN
 */

@Interceptor(priority = 2, name = "ARouter跳轉攔截器")
public class ARouterInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        Logger.v("######界面跳轉 : " + postcard.toString());

        //當前所有的權限
        String[] permissions = new String[]{
                "登錄",
                "綠色通道",//若目標此flag設定,則表示禁止跳轉
                "棧單例模式"//若目標設置此flag,則添加singTask標誌
        };
        int[] FLAGS_ALL = new int[]{
                ARouterConst.FLAG_LOGIN,
                ARouterConst.FLAG_FORCE_ACCESS,
                ARouterConst.FLAG_ACTIVITY_CLEAR_TOP
        };

        //當前所有權限對應的boolean值;爲false則對應權限設爲 ARouterConst.FLAG_NONE
        boolean[] FLAGS_ALL_VALUE = new boolean[]{
                DefaultPreferenceUtil.getInstance().hasLogin(),
                false,
                false
        };

        //當前所有的權限
        int currentFlags = Integer.MIN_VALUE;
        for (int position = 0; position < FLAGS_ALL.length; position++) {
            currentFlags |= FLAGS_ALL_VALUE[position] ? FLAGS_ALL[position] : ARouterConst.FLAG_NONE;
        }
        Logger.v("######當前所有權限 : " + Integer.toBinaryString(currentFlags));

        //目標界面需要的權限
        int requireFlags = postcard.getExtra() | Integer.MIN_VALUE;
        Logger.v("######目標所需權限 : " + Integer.toBinaryString(requireFlags));

        //如果需要的權限都已存在,則直接跳轉,不做處理
        if ((requireFlags & currentFlags) == requireFlags) {
            callback.onContinue(postcard);
            return;
        }

        //如果發現不一致,說明某些權限不存在,則需要依次判斷哪個權限不存在
        for (int position = 0; position < FLAGS_ALL.length; position++) {
            if ((requireFlags & FLAGS_ALL[position]) != 0 && (currentFlags & FLAGS_ALL[position]) == 0) {
                // TODO: 2018/1/20 沒有對應的f權限
                boolean consume = false;
                switch (position) {
                    case 0: //未登錄
                        consume = dispatchLogin(postcard, callback);
                        break;
                    case 1:
                        break;
                    case 2: //棧單例模式
                        consume = dispatchSingleTask(postcard, callback);
                        break;
                    default: {
                        callback.onInterrupt(new RuntimeException("沒有 " + permissions[position] + " 權限"));
                    }
                }

                if (!consume) {
                    callback.onInterrupt(new RuntimeException("界面無法跳轉"));
                }

                return;
            }
        }

        //權限定義錯誤
        RxBus.getInstance().post(new BaseEvent(Const.SHOW_TOAST, "未知權限"));
    }

    /**
     * 請求單例啓動
     *
     * 清除棧上其他活動
     */
    private boolean dispatchSingleTask(Postcard postcard, InterceptorCallback callback) {
        postcard.withFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        callback.onContinue(postcard);
        return true;
    }

    /**
     * 處理未登錄操作
     */
    private boolean dispatchLogin(Postcard postcard, InterceptorCallback callback) {
        RxBus.getInstance().post(new BaseEvent(Const.SHOW_LOGIN_DIALOG, null));
        return false;
    }


    @Override
    public void init(Context context) {
        // 攔截器的初始化,會在sdk初始化的時候調用該方法,僅會調用一次
    }

    /**
     * 更換意圖的跳轉路徑
     * 然後進行跳轉處理
     *
     * @param postcard 意圖
     * @param des      目的 string
     */
    private void replaceDes(Postcard postcard, String des) {
        //動態的修改postcard信息,更換跳轉路徑
        Postcard newPostcard = ARouter.getInstance().build(des);
        LogisticsCenter.completion(newPostcard);
        postcard.setPath(newPostcard.getPath()).setGroup(newPostcard.getGroup()).setDestination(newPostcard.getDestination());
    }
}

如代碼所示,可以分爲四個步驟:

  1. 獲取當前應用擁有的所有權限
  2. 獲取目標頁面所需的權限
  3. 進行判斷,若是目標頁面所需權限當前已擁有,則跳轉成功;否則進行步驟4
  4. 按順序判斷是什麼權限未滿足,通過 dispatch*** 方法進行處理

同時 項目中放置了很多方便操作的工具類以及代碼生成器;

如在 build.gradle 中添加了生成MVP模式類的代碼:

/**
 * Desc: generate mvp architecture code automatically
 *
 * @Des For example: gradle mvp -D domain="Test" -D path="/activity"
 * def domainParam = System.getProperty('domain')
 * def pathParam = System.getProperty('path')
 * 
 * */
task mvp_activity(type: GenerateMVPActivity) {
    group 'personal'
    description 'generate java code for MVP-Activity architecture'
    def domainParam = "Test"
    def pathParam = "/activity"
    if (domainParam && pathParam) {
        domain domainParam
        uiPath pathParam
    }
}

使用時將 domainParam 重新賦值,然後通過任務就可以生成相應的代碼。

任務執行可以通過AndroidStudio右側的任務列表進行:

這裏寫圖片描述

也可以在該任務上: 右鍵 => Assign ShortCut => 添加快捷鍵

這裏寫圖片描述

以後每次生成代碼,只需要修改domainParam 字段值,然後使用快捷鍵生成代碼即可,不過要注意: 不能使用相同名字的Activity及Fragment,會相互覆蓋;

功能一般而言足夠普通項目使用,只需要在使用時進行:修改包名,設置友盟KEY 等操作後就可以添加業務處理了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章