一只小安卓Clean架構的實踐和他的年終總結

前言

從自學安卓到畢業(yè)??然后工作已經快一年多了吧

這一年平時基本忙這公司的東西

自己想寫一些小項目,都是寫了一點就廢棄了

感覺完整的項目對個人的提升不大,因為大部分時間都在造輪子

總的來說,這一年還是做了一點微小的工作

My contributions this year
My contributions this year

有所成長,但是也知道自己欠缺的東西越來越多

前后待了兩個公司,都是小規(guī)模的創(chuàng)業(yè)公司

至今都是1個人(或者2個人)開發(fā)項目,感覺一直都在很低效很低效地工作

感覺android開發(fā)真是艱難啊,相比iOS

陸陸續(xù)續(xù)看過也練手過,MVP,MVVM,MVPVM之類的東西

當時并不能切身地感覺到它們的好處,現(xiàn)在想想真是圖樣圖森破

甚至以前我都不怎么喜歡用Fragment,因為發(fā)現(xiàn)Fragment很難管理

現(xiàn)在我開始在項目里面大量的使用Fragment,因為多數(shù)情況下它們可以復用,可以節(jié)省很多時間

一個人開發(fā)嘛,心里的苦只有自己知道??
其實是所有的鍋只能一個人背(哈哈

也開始瘋狂封裝一些基類來復用

比如一個顯示列表的Fragment基本就是傳入一個ViewModel和一個Item的布局文件就可以了

即便如此,雖然復用了大量代碼,但是限制也越來越多

每次產品經理一開口,我都感覺自己的代碼是一攤??屎

可能我能通過很好的封裝來解決ActivityFragment過重的問題,但是項目還是很難維護

尤其是當需求一直改變,新功能不斷迭代,項目的歷史負擔越來越大

KPI壓力倒是沒用…感覺我們的用戶都是iOS人群??

每隔一段時間看自己的代碼,我都感覺是在屎山里面翻滾...

所以V/P分離貌似成為了一種必需

盡管各種架構前期總有一些額外的工作,但是之后的開發(fā)會變得很輕松

在app體積不斷增大的時候,讓項目細分下來的codebase盡量小

又一個前言

最近了解并嘗試了一下clean架構,發(fā)現(xiàn)它真的很給力

好后悔自己沒有早點熟悉它并投入使用.


Clean架構可以使你的代碼有如下特性:

  • 獨立于架構
  • 易于測試
  • 獨立于UI
  • 獨立于數(shù)據(jù)庫
  • 獨立于任何外部類庫

如果你還不了解Clean架構,肯定要先去看下這個:

Uncle BobThe Clean Architecture

這里只是想講講自己關于Clean架構的一點實踐,

有問題大家一起探討下...

開始

試著寫了一個demo:Vincent

雖然只寫了一點點(然后廢棄了= =),但是Clean架構的樣子感覺有了(我是這么覺得哈哈

初衷是寫一個tubmlr樣子的weibo, 寫著寫著發(fā)現(xiàn)渣浪對第三放開發(fā)者的限制太多了.

發(fā)現(xiàn)授個權還要認證,還要申請一個藍V的微博…我感覺好難受

首先這張圖你肯定見過...

其實更直觀的,我們直接就著代碼來看3個module之間的依賴:

  • data(數(shù)據(jù)層
apply plugin: 'java'
def cfg = rootProject.ext;
dependencies {
    // ReactiveX
    compile cfg.dependencies["rxjava"]
    // Square
    compile cfg.dependencies["retrofit"]
    compile cfg.dependencies["converter-gson"]
    compile cfg.dependencies["adapter-rxjava"]
    compile cfg.dependencies["logging-interceptor"]
    compile cfg.dependencies["dagger"]
}
  • usecase(用例層 當然你叫domain也行
apply plugin: 'java'
dependencies {
    compile project(':data')
}

datausecase層都應該是java代碼,跟Android Framework無關

  • app(就是我們平時寫的那個app
  dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        // skip
        compile project(':usecase')
  }

然后你們感受一下:

  • 用例層從數(shù)據(jù)層獲取數(shù)據(jù)
  • 在用例里面進行處理業(yè)務邏輯(雖然我發(fā)現(xiàn)一般的項目根本沒有什么業(yè)務邏輯可言
  • 視圖層從用例層獲取數(shù)據(jù)然后顯示在界面上

Standing

然后講一下各種亂七八糟的東西

data層

entities:

實體類(最原始的數(shù)據(jù),不該隨著業(yè)務邏輯而改變

wrapper:

對實體類的一層封裝,在這里你可能要自己加一些東西:比如:

  • 處理timestamp,轉換輸出本地時間
  • 一些額外的參數(shù),比如數(shù)據(jù)邊界的驗證等等

當然這些你也可以放在View層的ViewModel里面
但是感覺放這里比較清晰

remote:

顧名思義嘛,這就是RemoteDataSource,在這里配置和封裝了(對于我來說)OkHttpRetrofit的接口

utils:

  • 自定義了一些transformer,比如切換線程和全局的錯誤處理
  • 網絡請求的Interceptor
  • 其他亂七八糟的

當時這樣寫了我就感覺特別酷炫(說出來別笑我...

因為可以在這里直接用一個java類來測試api,而不是編譯整個項目...

不對不對,是API...有一次我們的python過來說你Api這個類應該全大寫...

每次叫我們python過來看logcat的時候 = = 他都會在旁邊發(fā)牢騷: 安卓編譯怎么會這么慢....
臥槽我也不想啊

當然你會說用Postman不就好了...

但是這樣能有一個配置了OkHttpRetrofit的環(huán)境嘛...


usecase層

用例層就比較簡單了...因為一般的應用都沒有什么業(yè)務邏輯

舉個??,我要從一個從server端獲取微博(status)顯示到timeline列表上

首先我有一個Usecase的接口要繼承(<T>是網絡請求返回的數(shù)據(jù)類型

public abstract class Usecase<T> {
    protected abstract Observable<T> buildObservable();
}

具體代碼:

public class GetTimelineUsecase extends Usecase<Timeline> {
    private final RemoteRepository mRepository;
    private final SchedulerTransformer<Timeline> mSchedulerTransformer;
    private int page = 1;
    @Named("uid")
    private long uid;
    @Named("timeline_type")
    private String timeline_type;

    @Inject
    GetTimelineUsecase(RemoteRepository repository,
                       @Named("io_main") SchedulerTransformer schedulerTransformer,
                       @Named("timeline_type") String timeline_type,
                       @Named("uid") long uid) {
        mRepository = repository;
        mSchedulerTransformer = schedulerTransformer;
        this.uid = uid;
        this.timeline_type = timeline_type;
    }
}

在構造函數(shù)里面我們注入了:

  • 根據(jù)頁面邏輯相關的參數(shù)(timeline_type,uid)

  • Android Framework相關的東西(比如AndroidSchedulers.mainThread()

    因為這一層是apply plugin: 'java'嘛~

然后我需要有:

  • RemoteRepository(源,返回數(shù)據(jù)

  • SchedulerTransformer(切換線程,比如IO和UI線程之間

    這里我直接注入一個transformer...當然你也可以注入不同的thread

    ?

  • timeline_type,uid(接口的參數(shù)

    針對返回相同數(shù)據(jù)類型的接口 我們可以封裝在一個用例里面

我在buildObservable()中根據(jù)timeline_typeuid請求不同的接口:

   @Override
    protected Observable<Timeline> buildObservable() {
        switch (timeline_type) {
            case "home_timeline":
                return mRepository.home_timeline(page, feature)
                        .compose(mSchedulerTransformer);
            case "friends_timeline":
                skip;
            default:
                return Observable.error(new Throwable("Timeline type can't be null!"));
        }

然后在GetTimelineUsecase中會有一個excute()方法,我在這里處理業(yè)務邏輯:

public Observable<Timeline> excute(final boolean refresh) {
    return buildObservable()
            .doOnSubscribe(new Action0() {
                @Override
                public void call() {
                    if (refresh) {
                        page = 1;
                    }
                }
            })
            .doOnNext(new Action1<Timeline>() {
                @Override
                public void call(Timeline timeline) {
                    if (0 != timeline.getNextCursor()) {
                        ++page;
                    }
                }
            });
}

然后你會問業(yè)務邏輯在哪里(黑人問號???

我就說嘛,一般的項目沒什么業(yè)務邏輯....

這里不是有一點點處理分頁的邏輯(就這么多了

所以View層(或者說是你的Presenter)不用再關心分頁,不用有什么page或者cursor

只要在那邊一直調用excute()就好了,想到一個View層的設計原則

讓View盡可能的笨拙和被動

雖然會傳過來一個refreshBoolean值哈,但是可以認為這只是用戶操作(頁面邏輯),和業(yè)務邏輯無關.

PS:

在這里還自定義了一個注解

@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD})
public @interface Nullable {
}

因為這個module沒有support-annotation可以用

Dagger會要求如果你注入的參數(shù)是null,就必須用@Nullable注解,不然就會報錯.

在一些地方我需要用到它,來傳入一些默認值(比如null和0,因為可能這個請求參數(shù)用不到

另外,在注入String Long等類型的參數(shù)的時候,需要用@Name注解,不然編譯器肯定不知道你需要的是什么


View層

View層其實沒什么好講的,就是Databinding+Dagger+MVP

Dagger:

喜歡用Dagger的原因是因為它能迫使我理清所有的依賴關系

至于注入本身感覺也沒有那么方便,畢竟多了很多前期工作...

關于Dagger的使用,也是看了很多文章才拎清楚的

推薦一下frogermcs的博客~

MVP:

基本上是照著Google的android-architecture寫出來的

有很多分析的不錯的文章,我也是看了好久才搞清楚的

比如CameloeAnthony在簡書上的那個系列~

這里就不展開了…

EventBus:

項目里的事件總線用的是Eventbus.

不過漸漸發(fā)現(xiàn)代碼量一上去,各種event就會使邏輯變得很混亂

不知道Eventbus的正確打開方式到底是什么?

現(xiàn)在我反正是盡量少用...

DataBinding

其實用了之后感覺沒有一開始那么酷炫啦

雙向綁定什么的,實際使用場景很少...

而且Clean結構決定了View和真正的Model只會是單向的綁定

但是如果你還沒用過DataBinding,趕緊嘗試一下吧

其實Google的文檔寫的不是很全面,寫的最好的感覺還是大帥的博客

ViewModel:

既然是單向綁定,ViewModel扮演的角色就只是

  • 暴露出需要的properties
  • 讓View實現(xiàn)它的接口

當然關于ViewModel的寫法,講道理應該是對data層的model的一次映射,

但是感覺這樣要用手打好多代碼...如果AS有插件(類似GsonFormat之類的)我可能會這樣寫...

所以一般我們直接extends或者wrap某個model就好了…再調用getter方法獲得properties

Tips

其實用ViewModel的一個好處是可以很方便的封裝一些方法.

比如圖片加載庫,你可能會覺得圖片加載庫還要封裝,不是只要一句話:

load(url).into(imageView);

對啊,但是可能你因為Glide的方法數(shù)太多,要換成Picasso,這下不就麻煩了

但是當你把所有圖片加載的場景(一般的...)都寫在@BindingAdapter里面

舉個??:

    @BindingAdapter("avatar")
    public static void loadAvatar(ImageView view, String url) {
        Glide.with(view.getContext()).load(url).into(view);
    }

    @BindingAdapter("photo")
    public static void loadPhoto(ImageView view, String uri) {
        Glide.with(view.getContext()).load(Uri.parse(uri)).into(view);
    }

這樣到時候全局意義上的替換一個庫就很方便了

Rxjava

Subscriber來代替Callback,在Presenter里面就全部都是從上往下的Rx的stream

舉個??,一個列表加載數(shù)據(jù):

    public void loadMore() {
        if (dataInTransit || reachBottom) return;
        mSubscriptions.add(mUsecase.execute(false) // boolean: needRefresh
                .doOnSubscribe(() -> dataInTransit = true)
                .doOnTerminate(() -> dataInTransit = false)
                .doOnSubscribe(mView::startLoading)
                .doOnTerminate(mView::stopLoading)
                .doOnError(mView::showError)
                .subscribe(timeline -> {
                    if (timeline.getNextCursor() == 0) {
                        reachBottom = true;
                        mView.showNoMore();
                    } else {
                        mView.setData(timeline.getStatuses(), false);
                    }
                }));
    }

寫起來特別舒服~

關于RxJava,響應式編程什么的已經不是一個新鮮玩意兒,在這里就不贅述了

安利一下,感覺Awesome-RxJava列出的一些資料都非常不錯~

最后

其他還有一些東西...之后再補上了

總之,Clean架構是個蠻有意思然后很實用的東西

大家有時間可以了解一下~

最后,Everybody 新年快樂?????? 新的一年里都能成為更好的自己~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抢呆,隨后出現(xiàn)的幾起案子厚掷,更是在濱河造成了極大的恐慌静檬,老刑警劉巖童太,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赏迟,死亡現(xiàn)場離奇詭異约素,居然都是意外死亡洽故,警方通過查閱死者的電腦和手機贝攒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來时甚,“玉大人隘弊,你說我怎么就攤上這事』氖剩” “怎么了梨熙?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長刀诬。 經常有香客問我咽扇,道長,這世上最難降的妖魔是什么陕壹? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任质欲,我火速辦了婚禮,結果婚禮上糠馆,老公的妹妹穿的比我還像新娘嘶伟。我一直安慰自己,他們只是感情好又碌,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布九昧。 她就那樣靜靜地躺著盛霎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪耽装。 梳的紋絲不亂的頭發(fā)上愤炸,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音掉奄,去河邊找鬼规个。 笑死,一個胖子當著我的面吹牛姓建,可吹牛的內容都是我干的诞仓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼速兔,長吁一口氣:“原來是場噩夢啊……” “哼墅拭!你這毒婦竟也來了?” 一聲冷哼從身側響起涣狗,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤谍婉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后镀钓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體穗熬,經...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年丁溅,在試婚紗的時候發(fā)現(xiàn)自己被綠了唤蔗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡窟赏,死狀恐怖妓柜,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情涯穷,我是刑警寧澤棍掐,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站求豫,受9級特大地震影響塌衰,放射性物質發(fā)生泄漏。R本人自食惡果不足惜蝠嘉,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一最疆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚤告,春花似錦努酸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仍源。三九已至,卻和暖如春舔涎,著一層夾襖步出監(jiān)牢的瞬間笼踩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工亡嫌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嚎于,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓挟冠,卻偏偏與公主長得像于购,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子知染,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內容