1. 程式人生 > >使用MVVM嘗試開發Github客戶端及對程式設計的一些思考

使用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模式和設計理念相關部落格已經爛大街了,而且我也不認為我能夠講的比別人更透徹。

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

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

回顧本文,我希望本文的每一小節都能給您帶來有益的東西,它可能是一種積極狀態的傳遞,也可能某小節涉及的知識點讓您感興趣,或是其他——而並非是“講的什麼東西介紹了一大堆三方庫”或者 “用這麼多偏門的庫,真裝13”這些悲觀的狀態。

我最終推薦給您這篇文章和這個專案,它代表的是這一階段持續學習的結果,至少目前於我來說它是優秀的專案(對您來說可能嘈點滿滿),很期待不久之後的我能夠用懷疑的眼光去看待這個專案,那將意味著下一階段的進步。

差點忘了專案地址:https://github.com/qingmei2/MVVM-Rhine

關於我

Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github

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