使用MVVM嘗試開發Github客戶端及對編程的一些思考

本文已授權 微信公眾號 玉剛說@任玉剛)獨家發布。

本文中我將嘗試分享我個人 搭建個人MVVM項目 的過程中的一些心得和踩坑經歷,以及在這過程中目前對 編程本質 的一些個人理解和感悟,特此分享以期討論及學習進步。

緣由

最近在嘗試搭建自己理解的 MVVM模式 的應用程序,在這近一個月中,我思考了很多,也參考了若干Github上MVVM項目源碼,并從中獲益匪淺。

我根據所得搭建了一個MVVM開發模式的Github客戶端,并托管在了自己的github上:

MVVM-Rhine: MVVM+Jetpack的Github客戶端

創建這個項目的原因是我想有一個自己寫的 Github客戶端 方便我查看,目前我基本實現了自己的目標,App整體的效果是這樣的:


在開發過程中,我根據自己對于編程的理解,在技術選型中,加了一些自己喜歡的庫,寫了一些自己比較滿意的風格的代碼,特此和大家一起分享我的所得,謬誤之處,歡迎拍磚。

1.我為什么選擇Kotlin?

回顧近半年來,我博客中的編程語言使用的清一色是 Kotlin,這樣做的最初目的是督促自己學習Kotlin。

我曾在 某篇文章 中這樣聲明我用Kotlin的原因:

不僅如此,Kotlin語言國外已經有相當的熱度了,只是目前相比Java,國內還沒有完全推廣起來而已。

此外,Kotlin的一些特性能夠讓我們實現Java實現不了的東西(不是空安全,無需findViewById這些基本的語法糖),對于某些設計點,Kotlin是Java無法替代的,這點我會在后文中提到。

2.MVVM的本質:異步觀察者模式

很多朋友對RxJava的理解是 鏈式調用線程切換 等等,對我來說,在RxJava的逐漸使用過程中,我對它的理解慢慢趨于 異步 一詞——RxJava 強迫開發者從思想上將異步代碼同步代碼歸于一統,對于任何業務功能,都可以抽象為一個可觀察的對象。

MVVM的本質亦是如此,DataBinding 幫我們為 數據驅動視圖 提供了可實現的方案,因此它成為了大多數MVVM項目中的核心庫。

MVVM觀察者模式的本質也意味著,即使沒有DataBinding,我們通過RxJava或者其他方式也能夠實現 MVVM,只不過DataBinding更方便搭建MVVM而已。

這里不拿MVC、MVP和MVVM進行比較,因為不同的架構思想,都有不同的優劣勢,我非常沉迷于RxJava和其優秀的思想,我認為它的思想相當一部分和MVVM不謀而合,因此我更傾向使用MVVM,配合以RxJava,能夠讓代碼更加賞心悅目。

3.Android Jetpack: Architecture Components

Android Jetpack(下稱Jetpack) 是Google今年IO大會上正式推出官方的新一代 組件、工具和架構指導 ,旨在加快開發者的 Android 應用開發速度:

這是一套非常迷人的架構組件,Google今年還同步(其實晚了2個月)開源了一個Jetpack的示例項目 Sunflower

這個示例項目有著豐富的學習價值,也很方便開發者迅速上手并熟悉Jetpack的組件——當然,只是上手當然滿足不了我的需求,我想通過自己參與一個項目的實踐來深入了解并感受這些組件,于是 我在這個項目中使用了這些組件

我簡單通過個人感受分別闡述一下這些組件真正融入MVVM項目中的感受:

3.1 DataBinding

MVVM的 核心組件,通過良好的設計,我的項目中避免了95%以上的 冗余代碼—— 它的作用簡單直接,就是 數據驅動視圖,我再也不需要去通過控件設置UI,相反,所有UI的變動都交給了 被觀察的成員屬性 去驅動。

View的點擊事件:

<ImageView
    android:id="@+id/btnEdit"
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:src="@drawable/ic_edit_pencil"
    app:bind_onClick="@{ () -> delegate.edit() }" />

ImageView的url加載:

<ImageView
    android:id="@+id/ivAvatar"
    android:layout_width="80dp"
    android:layout_height="80dp"
    app:bind_imageUrl_circle="@{ delegate.viewModel.user.avatarUrl }" />

TextView的設置值:

<TextView
    android:id="@+id/tvNickname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{ delegate.viewModel.user.name }" />

有同學覺得這太簡單,那我們換一些有說服力的。

你還在 Activity 代碼配置 RecyclerView?直接xml里一次性配置RecyclerView,包括 滑動動畫下拉刷新點擊按鈕列表滑動到頂部

<android.support.v4.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:onRefreshListener="@{ () -> delegate.viewModel.queryUserRepos() }"  // 刷新監聽
    app:refreshing="@{ safeUnbox(delegate.viewModel.loading) }">    // 刷新狀態

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        app:bind_adapter="@{ delegate.viewModel.adapter }"   // 綁定Adapter
        app:bind_scrollStateChanges="@{ delegate.fabViewModel.stateChangesConsumer }"
        app:bind_scrollStateChanges_debounce="@{ 500 }"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        tools:listitem="@layout/item_repos_repo" />

</android.support.v4.widget.SwipeRefreshLayout>

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fabTop"
    android:src="@drawable/ic_keyboard_arrow_up_white_24dp"
    app:bind_onClick="@{ () -> recyclerView.scrollToPosition(0) }"    // 點擊事件,列表直接回到頂部
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

還在配置 ViewPager+Fragment+BottomNavigationView的切換效果,包括ViewPager滑動切換監聽,自動配置Adapter,BottomNavigation的點擊監聽, 我們都在Xml聲明好,交給DataBinding就行了:

<android.support.v4.view.ViewPager
    android:id="@+id/viewPager"
    app:onViewPagerPageChanged="@{ (index) -> delegate.onPageSelectChanged(index) }"
    app:viewPagerAdapter="@{ delegate.viewPagerAdapter }"
    app:viewPagerDefaultItem="@{ 0 }"
    app:viewPagerPageLimit="@{ 2 }" />

<android.support.design.widget.BottomNavigationView
    android:id="@+id/navigation"
    app:bind_onNavigationBottomSelectedChanged="@{ (menuItem) -> delegate.onBottomNavigationSelectChanged(menuItem) }" 
    app:itemBackground="@color/colorPrimary"
    app:itemIconTint="@drawable/selector_main_bottom_nav_button"
    app:itemTextColor="@drawable/selector_main_bottom_nav_button"
    app:menu="@menu/menu_main_bottom_nav" />

篇幅所限,省略了一些常見的屬性,上述的所有源碼,你都可以在我的項目中找到。

我的意思不是想說 DataBinding 多么強大(它確實可以實現足夠多的功能),對我而言,它最強大的好處是—— 節省了足夠多UI控件的設置代碼,讓我能夠 抽出更多時間去寫純粹業務邏輯的代碼。

有朋友覺得DataBinding最大的問題就是不好Debug,我的解決方案是統一 狀態管理,這個后文再提。

3.2 Lifecycle

Lifecycle 讓我能夠更專注于 業務邏輯 而非 生命周期,我認為這是不可代替的,如果你熟悉 Lifecycle,你可以看我的這篇文章:

Android官方架構組件Lifecycle:生命周期組件詳解&原理分析

Lifecycle能夠讓我想要的組件也擁有 生命周期(實際上是對生命周期容器的觀察),比如,我不再需要讓Activity或者Fragment在onCreated()中去請求網絡,取而代之的是:

class LoginViewModel(private val repo: LoginDataSourceRepository) : BaseViewModel() {

  override fun onCreate(lifecycleOwner: LifecycleOwner) {
          super.onCreate(lifecycleOwner)

          // 自動登錄
          autoLogin.toFlowable()
                .filter { it }
                .doOnNext { login() }
                .bindLifecycle(this)
                .subscribe()
    }
}

上文的示例代碼展示了,Login界面的自動登錄邏輯(當然也可以是網絡請求展示數據的邏輯),ViewModel檢測到了Activity的生命周期并自動調用了onCreate()函數——我并沒有通過Activity去調用它。

3.3 ViewModel

ViewModel能夠檢測到持有者的 生命周期,并避免了 橫豎屏切換時額外的代碼的配置,它的內部是通過一個不可見的 Fragment 對數據進行持有,并在真正該銷毀數據的時候去銷毀它們。

同時,它是MVVM中的 核心組件,我在項目的規范定義中,layout中所有的屬性配置都應該依賴于ViewModel中的MutableLiveData屬性:

class LoginViewModel(
        private val repo: LoginDataSourceRepository
) : BaseViewModel() {
 
    val username: MutableLiveData<String> = MutableLiveData()  // 用戶名輸入框
    val password: MutableLiveData<String> = MutableLiveData()  // 密碼輸入框

    val loading: MutableLiveData<Boolean> = MutableLiveData()   // ProgressBar
    val error: MutableLiveData<Option<Throwable>> = MutableLiveData()  // Errors

    val userInfo: MutableLiveData<LoginUser> = MutableLiveData()   // 用戶信息

    private val autoLogin: MutableLiveData<Boolean> = MutableLiveData() // 是否自動登錄

    // ......
}

3.4 LiveData

參照 RxJava 豐富的生態圈, LiveData 看起來似乎實在雞肋,但是DataBinding在最近的版本中提供了對 LiveData 的支持,考慮再三,我采用了 LiveData,正如上文示例代碼,配合以 ViewModel, UI完整的驅動系統被搭建起來。

LiveData并非一無是處,它確實值得我作為依賴添加進自己的項目中,原因有二:

  • 原生支持 DataBinding 和 Room

實際上 Paging 也是支持的,但是我沒有用到Paging

  • 安全的數據更新

RxJava在子線程進行UI的更新依賴于 observerOn(AndroidSchedudler.mainThread()),但是LiveData不需要,你只需要通過 postValue(),就能安全的進行數據更新,就像這樣:

 val loading: MutableLiveData<Boolean> = MutableLiveData()

this.loading.postValue(value)    // 數據的設置會在主線程上

但是我仍然需要面臨一個問題,就是LiveData的生態圈實在沒辦法和 RxJava 相關的庫對比,想要通過LiveData的操作符進行業務處理實在不靠譜,因此我選擇將LiveDataobserve()變成RxJavaFlowable

private val autoLogin: MutableLiveData<Boolean> = MutableLiveData()

 autoLogin.toFlowable()   // 變成了一個Flowable
                .filter { it }
                .doOnNext { login() }
                .bindLifecycle(this)
                .subscribe()

得益于 kotlin 強大的 擴展函數,兩者之間的融合如 絲滑般的流暢

fun <T> LiveData<T>.toFlowable(): Flowable<T> = Flowable.create({ emitter ->
    val observer = Observer<T> { data ->
        data?.let { emitter.onNext(it) }
    }
    observeForever(observer)

    emitter.setCancellable {
        object : MainThreadDisposable() {
            override fun onDispose() = removeObserver(observer)
        }
    }
}, BackpressureStrategy.LATEST)

現在,我們一邊享受著 LiveData 安全的數據更新和DataBinding的原生支持,一邊享受 RxJava 無以倫比 強大的操作符和函數式編程思想,這簡直讓我如沐春風。

3.5 Room

ORM數據庫,市面上太多了不解釋,我選擇使用它的原因有二:

  • 1.Google爸爸官方出品,無腦用
  • 2.原生支持RxJavaLiveData, 無腦用

真香。

3.6 Navigation

Google官方 單Activity多Fragment 的架構組件,如果你不是很熟悉,可以參考這篇文章:

Android官方架構組件Navigation:大巧不工的Fragment管理框架

很感謝文章吹來之后,很多同學對文章的肯定,我也相信很多同學已經熟悉甚至嘗試上手了這個庫,我這次嘗試在項目中使用它,原因是,我想試試 它是不是真的像我文章吹的那么好用

經實戰,初步結果是:

可以用,但沒必要。

在大多數情況下,Navigation都顯得非常穩健,但是 框架是死的,但是需求是千變萬化的,我總是不可避免去面對一些問題:

  • 1.官方提供了NavigationToolbarBottomNavigationView的原生支持,但是令我哭笑不得的是,Navigation內部對Fragment的切換采用的是replace(),這意味著,每次點擊底部導航控件,我都會銷毀當前的Fragment,并且實例化一個新的Fragment

  • 2.很多APP采用了Home界面,雙擊返回才會退出Application的需求,正常我們可以重寫Activity的onBackPress()方法,而使用了Navigation,我們不得不把導航的返回行為委托給了Navigation

class MainActivity : BaseActivity<ActivityMainBinding>() {

    override val layoutId = R.layout.activity_main

    override fun onSupportNavigateUp(): Boolean =
            findNavController(R.id.navHostFragment).navigateUp()

     // ...
}

當然,這些問題都是有解決方案的,以BottomNavigationView每次切換都會銷毀當前Fragment并實例化新的Fragment為例,我的建議是:

對根布局的View使用Navigation,界面內部的布局采用常規實現方式(比如ViewPager+Fragment)。

比如我在MainActivity中聲明NavHostFragment:

    <android.support.constraint.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <fragment
            android:id="@+id/navHostFragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/navigation_main" />

    </android.support.constraint.ConstraintLayout>

我的BottomNavigationView導航界面,則是一個MainFragment:

<android.support.constraint.ConstraintLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="0dp"
        android:layout_height="0dp"" />

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:menu="@menu/menu_main_bottom_nav" />

</android.support.constraint.ConstraintLayout>

我保證 只有根布局的頁面通過Navigation進行導航,至于NavigationBottomNavigationView的原生支持,我選擇無視......

總而言之,對于是否使用Navigation,我的建議是持保守態度,因為這個東西和其它三方庫不同,Navigation的配置是 項目級 的。

4. 天馬行空:RxJava

關于項目中RxJava相關庫的配置,我選擇了這些:

我是RxJava的重度依賴使用者,它讓我沉迷于 業務邏輯的抽象,嘗試將所有代碼歸 異步 于一統,因此我依賴了這些庫。

5. 依賴注入:Kodein

編程的樂趣在于 探索,對于Android開發者來說,Dagger2 可能會是更多開發者的首選,但對于一個 探索性質更多 的項目來說,Dagger2 并不是最優選,最終我選擇了Kodein:

Kodein官網:Painless Kotlin Dependency Injection

如果您完整的閱讀了 《Kotlin 實戰》這本書,你能在書末的附錄中找到選擇它的原因:

常見的Java依賴注入框架,比如 Spring/Guide/Dagger,都能很好地和Kotlin一起工作,如果你對原生的Kotin方案感興趣,試試 Kodein, 它 提供了一套漂亮的DSL來配置依賴,而且它的實現也非常高效。

總結一下我個人的感受:

  • 更Kotlin,整個框架都由Kotlin實現
  • 實現方式依賴于 Kotlin 的 屬性委托
  • 很簡潔,相比復雜的Dagger,上手更簡單
  • 超級漂亮的DSL && 說出去更唬人......

Http網絡請求 相關為例,來看看依賴注入的代碼:

很漂亮,對吧?

當然,對于依賴注入庫,Dagger2是一個不會錯的選擇,但是如果僅僅只是個人項目,或者您已經厭倦了Dagger的配置,Kodein是一個不錯的建議。

如果你對 Kodein 感興趣,可以參考這篇文章,參考本文的項目代碼,相信很快就能上手:

告別Dagger2,Android的Kotlin項目中使用Kodein進行依賴注入

6.函數式支持庫:Arrow

對于Kotlin的各種優點,函數是第一等公民 是一個無法忽視的閃光點,它與其他簡單的語法糖不同,它能夠讓你的代碼更加優雅。

Arrow是提供了一些簡單函數式編程的特性,利用Arrow提供的各種各樣的函子,你的代碼可以更加簡潔并且優雅。

比如,配合RxJava,你可以實現這樣的代碼以避免各種分支的處理,比如隨時都有可能的if..else(),并將這些額外的操作放在最終的操作符中(Terminal Operator)去處理:

interface ILoginLocalDataSource : ILocalDataSource {

    fun fetchPrefsUser(): Flowable<Either<Errors, LoginEntity>>
}

class LoginLocalDataSource(
        private val database: UserDatabase,
        private val prefs: PrefsHelper
) : ILoginLocalDataSource {

    override fun fetchPrefsUser(): Flowable<Either<Errors, LoginEntity>> =
            Flowable.just(prefs)
                    .map {
                        when (it.username.isNotEmpty() && it.password.isNotEmpty()) {
                            true -> Either.right(LoginEntity(1, it.username, it.password))
                            false -> Either.left(Errors.EmptyResultsError)
                        }
                    }
}

現在我們將特殊的分支(數據錯誤)也同樣像正常的流程一樣交給了 Either<Errors, LoginEntity>統一返回,只有我們在真正需要使用它們時,它們才會被解析:

 fun login() {
        when (username.value.isNullOrEmpty() || password.value.isNullOrEmpty()) {
            true -> applyState(isLoading = false, error = Errors.EmptyInputError.some())
            false -> repo
                    .login(username.value!!, password.value!!)   // 返回的是 Flowable<Either<Errors, LoginUser>>
                    .compose(globalHandleError()) 
                    .map { either ->      // 用到的時候再處理它
                        either.fold({
                            SimpleViewState.error<LoginUser>(it)
                        }, {
                            SimpleViewState.result(it)
                        })
                    }
                    .startWith(SimpleViewState.loading())
                    .startWith(SimpleViewState.idle())
                    .onErrorReturn { it -> SimpleViewState.error(it) }
                    .bindLifecycle(this)
                    .subscribe { state ->
                        // ...
                    }
        }
    }

在函數式編程的領域,我只是一個滿懷敬意且不斷學習探索的新人,但是它的好處在于,即使沒有完全理解 函數式編程 的思想,我也可以通過運用一些簡單的函子寫出更加Functional的代碼。

7. 其他庫

除上述庫之外,我還引用了目前比較優秀的三方庫:

基于OkHttp的 網絡請求庫Retrofit,不贅述。

Glide 和 Timber,已經被大眾所熟知的 圖片加載庫 和 小巧精致的 日志打印庫,不贅述。

DslAdapter 是低調的Yumenokanata開發的RecyclerViewAdapter,API的DSL設計加上對 DataBinding 的支持,我認為我還遠遠沒達到寫這個庫的水平,因此在閱讀完源碼之后,我選擇使用它。

8. 面向工具編程:模版插件

無論是MVP還是MVVM,對于一種開發模式而言,代碼規范是很重要的,這意味著界面的實現總是需要用 同一種開發模式 進行規范化。

以MVP為例,標準的MVP,實現一個Activity的容器頁面,我們需要定義Contract和其對應的ViewPresenter,Model層的接口及其實現類,這就引發了另外一個問題,類似這種死板的開發模式的流程是否太繁瑣(即簡單的界面是否就沒寫這么多接口類的必要)?

我不這樣認為,模版代碼意味著開發的規范,這在團隊開發中尤其重要,這樣能夠保證項目品質的穩定性和一致性,并且便于擴展,對于繁瑣的生成重復性模版代碼的情況,我認為MVP的代表性框架 MVPArms做出了非常值得學習的方案,即配置模版插件

因此我也花了一點時間配置了一套屬于自己MVVM開發模式的模版插件,對于每個界面的初始化,可以很方便一鍵生成:

就這樣幾步,Activity/Fragment,ViewModel,ViewDelegate以及依賴注入的KodeinModule類,都通過模版插件自動生成,我只需要關注UI的繪制和業務邏輯的編寫即可。

無論是哪種開發模式,我認為模版插件都是一個能大大提高開發效率的工具,而且它的學習成本并不高,以我個人經驗,即使沒有相關經驗,也只需要3~4小時,就能開發出一套屬于自己的模版插件。

9.沒有使用的一些嘗試

9.1 組件化/模塊化開發

從我個人經驗來看,對于簡單的項目并不需要進行復雜的模塊化配置,因為開發者和維護者也只有我一個人。

9.2 Paging和WorkManager

這兩個也是 Android Jetpack 的架構組件,但我并沒有使用它們。

Paging是一個優秀的庫,我曾舉出它的優點(參考我的這篇文章),但是正如有朋友提到的,它的缺點很明顯,那就是Paging本身是對RecyclerView.Adapter的繼承,這意味著使用了Paging,就必須拋棄其他的Adapter庫,或者自己造輪子,最終我選擇了擱置。

WorkManager的原因就很簡單了,項目中的功能暫時用不到它....

9.3 事件總線

說到事件總線,國內比較容易被提及的有 EventBusRxBus,此外之前還看到某位大佬曾經分享過 LiveDataBus,印象很深刻,但是文章找不到了。

沒有采用事件總線的原因是,我已經有RxJava了。

有同學說既然你有RxJava,為什么不使用RxBus呢,因為對于依賴來說并沒有額外的負擔?

對此我推薦這篇文章放棄RxBus,擁抱RxJava:為什么避免使用EventBus/RxBus

引用文章中作者@W_BinaryTree對Jake Wharton對RxBus的評價翻譯:

W_BinaryTree的相關文章寫的都很有深度,我讀完很受啟發,冒昧推薦一下這位作者。

我認為RxJava本身就是對發布-訂閱者模式最優秀的體現,我盡量保證我的工程中處處都由RxJava去串聯就夠了。

于我個人而言,我完全贊同沒有引入RxJava的項目中使用EventBus,但是我確實不推薦RxBus,因為這意味著業務模塊之間層級設計得不清晰,才會導致全部交由RxJava中全局的Subject的訂閱情況的產生。

9.4 協程

協程的整體替換也在我下一步的學習計劃中。

這需要一段時間的發展,因為我認為目前協程還沒有發展足夠的生態環境——我更期待更多類似 retrofit2-kotlin-coroutines-adapter這樣優秀的拓展庫,能夠讓我下決定把所有RxJava的代碼給替換掉。

目前項目中,Room,網絡請求以及Databinding依賴的LiveData,都是通過RxJava進行編織串在一起的,這些代碼糅合很深,因此Kotlin1.3發布后(協程從實驗性的功能正式Release),我只先嘗試性的使用了類似 Result 這樣的API在異常處理上代替ArrowEither, 而協程則處于觀察狀態。

此外,我還沒有開始深入學習協程,從新手角度來看,可能還需要一段時間學習深入并理解它,因此我期待更多關于協程的分析和相關分享的文章。

10.關于狀態管理

狀態的管理一直是爭論不休的話題,甚至基于狀態管理還引申了 MVI (Model-View-Intent)的開發模式,關于MVI中文相關的博客我推薦這篇文章:

從狀態管理(State Manage)到MVI(Model-View-Intent)

這是一篇分析非常透徹的文章,閱讀之如飲甘怡,其中最重要的優勢便是對狀態額統一管理,讀后收獲甚豐,并做出了一些實驗性的嘗試,篇幅所限,不再贅述,詳情請參考 項目中ViewModel 的源碼。

11.感受

MVVM模式和設計理念相關博客已經爛大街了,而且我也不認為我能夠講的比別人更透徹。

我寫本文的原因是分享自己對于編程本質的理解,于我對編程的認知,探索過程中所帶來的樂趣成就感才是最重要的,追究本質可能是探索創造

我不喜歡拘泥于固定的開發模式,日復一日的重復操作讓我想起了工廠的流水線,編程不同,每個人的代碼風格的迥異背后代表著思想的碰撞,這是很多工作不能給予我的。

回顧本文,我希望本文的每一小節都能給您帶來有益的東西,它可能是一種積極狀態的傳遞,也可能某小節涉及的知識點讓您感興趣,或是其他——項目本身意義和這種收獲 相比反而不大,因為每個人的思想不同,對于MVVM的理解也不同。

因此,我不敢妄言這個項目代表了MVVM的規范,但至少目前我對它的設計很滿意(對您來說可能嘈點滿滿),它代表了我是這一階段持續學習的結果,,很期待不久之后的我能夠用懷疑的眼光去看待這個項目,那將意味著下一階段的進步。

項目地址:https://github.com/qingmei2/MVVM-Rhine

--------------------------廣告分割線------------------------------

系列文章

爭取打造 Android Jetpack 講解的最好的博客系列

Android Jetpack 實戰篇


關于我

Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ??,也歡迎關注我的個人博客或者Github

如果您覺得文章還差了那么點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容