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

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

Android Jetpack 實戰篇git

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

原因

最近在嘗試搭建本身理解的 MVVM模式 的應用程序,在這近一個月中,我思考了不少,也參考了若干Github上MVVM項目源碼,並從中獲益匪淺。數據庫

我根據所得搭建了一個MVVM開發模式的Github客戶端,並託管在了本身的github上:編程

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

建立這個項目的緣由是我想有一個本身寫的 Github客戶端 方便我查看,目前我基本實現了本身的目標,App總體的效果是這樣的:網絡

在開發過程當中,我根據本身對於編程的理解,在技術選型中,加了一些本身喜歡的庫,寫了一些本身比較滿意的風格的代碼,特此和你們一塊兒分享個人所得,謬誤之處,歡迎拍磚。架構

1.我爲何選擇Kotlin?

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

我曾在 某篇文章 中這樣聲明我用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的規範,但至少目前我對它的設計很滿意(對您來講可能嘈點滿滿),它表明了我是這一階段持續學習的結果,,很期待不久以後的我可以用懷疑的眼光去看待這個項目,那將意味着下一階段的進步。

項目地址:github.com/qingmei2/MV…

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

關於我

Hello,我是卻把清梅嗅,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人博客或者Github

若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章
相關標籤/搜索