嗶哩嗶哩Android客戶端——多媒體選擇器boxing開源

開源過程是坎坷的,道路是曲折的,但前路是光明的。

Github鏈接--Bilibili/boxing

歡迎pypr,star,issue。

開端

整個工程的開始于2015年。
回顧過去,業務層和邏輯層是完全耦合的。
業務快速的迭代很容易令開發工作變快變糙變莽(技術水平未達標也是原因之一),一次改版可能要對原有的代碼做侵入式修改,而根據開/閉原則,這是不可接受的。

舊問題

  • 由于設計欠缺優雅性導致功能擴展性差
  • 缺少單元測試無法保證程序的魯棒性
  • 業務,邏輯和UI的代碼耦合嚴重

2016年開始重構整個功能
目標

  • 業務層,邏輯層和UI層分離
  • 只依賴基礎庫
  • 易于業務擴展
  • 添加單元測試
  • 支持只使用Fragment

方案

  • 使用MVP模式
  • 只依賴support v4, v7
  • 通過接口和抽象類進行依賴反轉
  • 使用Junit4 + Mokito + Espresso做測試

talk is free, show me the code.

時序圖

啟動Activity的情況

boxing_sequenece.png

概要類圖

boxing_class.png

簡潔起見,只畫出兩種關系,橫線是組合,豎線是繼承。根據依賴反轉原則,橫線指向的應該是抽象類或者接口(圓形)。所以,View層,Presenter層和Module層已經分離了。

設計接口

要只依賴于基礎庫,圖片加載庫和裁剪庫的實現就要分離出去,通過接口的形式在內部使用。

public interface IBoxingMediaLoader {
    /**
     * display thumbnail images for a ImageView.
     *
     * @param img     the display ImageView.
     * @param absPath the absolute path to display.
     * @param width   the resize with for the image.
     * @param height  the resize height for the image.
     */
    void displayThumbnail(@NonNull ImageView img, @NonNull String absPath, int width, int height);

    /**
     * display raw images for a ImageView, need more work to do.
     *
     * @param img      the display ImageView.
     * @param absPath  the absolute path to display.
     * @param callback the callback for the load result.
     */
    void displayRaw(@NonNull ImageView img, @NonNull String absPath, IBoxingCallback callback);
}
  • 加載縮略圖
    縮略圖是根據界面可以知道resize寬高,不需要回調。
  • 加載原圖
    原圖可能很大,很長,所有是沒有resize的,通過絕對路徑去解析顯示,同時提供加載成功/失敗的回調。
public interface IBoxingCrop {

    /***
     * start crop operation.
     *
     * @param cropConfig  {@link BoxingCropOption}
     * @param path        the absolute path of media.
     * @param requestCode request code for the crop.
     */
    void onStartCrop(Context context, Fragment fragment, @NonNull BoxingCropOption cropConfig,
                     @NonNull String path, int requestCode);

    /**
     * get the result of cropping.
     *
     * @param resultCode the code in {@link android.app.Activity#onActivityResult(int, int, Intent)}
     * @param data       the data intent
     * @return the cropped image uri.
     */
    Uri onCropFinish(int resultCode, Intent data);
    }
  • 開始裁剪,啟動者可能是Activity或Fragment,需要額外的裁剪參數
  • 裁剪結束,通過Intent取到結果Uri

分離實現庫OK。

單元測試

單元測試的難易程度與耦合度成正比,簡而言之,優雅的設計令單元測試的編寫變得簡單。
構造Mock類不再困難重重,接口方法實現單一功能,Presenter層和Model層的測試通過Junit4 + Mokito實現,UI層通過Espresso實現。

Only Fragment

參考Google MVP,Activity只作為Presenter和View連接的載體,用Fragment實現View接口,因此可以單獨使用Fragment作為功能主體。

   // 初始化AbsBoxingPickerFragment
    Boxing.get().setupFragment(AbsBoxingPickerFragment, new Boxing.OnFinishListener() {
        @Override
        public void onFinish(Intent intent, @Nullable List<BaseMedia> medias) {
           // 通過回調接口取到結果         
        }
    });

最后總結

抽象能力非常重要,需要不斷地練習。

UML圖很重要,強烈推薦Bob大叔的UML:Java程序員指南(雙語版),暢快淋漓的閱讀快感。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容