Share
Embed
Android MvRx Framework 介紹
2018/10/24 Android Taipei 分享
介紹如何使用 Airbnb 的 MvRx Framework
用 React, Redux like 的方式開發 App
Chien Shuo (Kros)
October 24, 2018
Transcript
115 slides
-
Outline • 什什麼是 MvRx (唸作 mavericks) • Core Concepts •
實作細節 • Testing • 其他 -
什什麼是 MvRx • Introducing MvRx: Android on Autopilot (2018/08/28) https://medium.com/airbnb-engineering/introducing-mvrx-
android-on-autopilot-552bca86bd0a • The new framework for Android is fully native but eliminates 50– 75% of product code. • ⼀一個原⽣生 Android 的框架,可以⼤大幅減少撰寫的程式碼 -
幾個開發上的問題 • 複雜的 Android Lifecycle 問題 • 在 onSaveInstanceState 的處理理邏輯,與
view state 狀狀態儲存問題 • 在網路路或資料庫的非同步 (asynchronous requests) 呼叫時, onSuccess, onFailure 的處理理,thread 切換等問題 • Android 程式中預設都是 main thread,能否更更簡單的做 Threading 切 換,把複雜的計算放到 background thread -
什什麼是 MvRx • 為了了解決這些問題,Airbnb ⾃自⾏行行開發了了⼀一套框架 • ⽬目的是讓使⽤用者可以按照固定的架構,寫出⾼高品質、好維護、效能佳、 Bug 少的 Android
App • 已經⼤大幅在 Airbnb 的產品上採⽤用 -
什什麼是 MvRx • MvRx is Kotlin first and Kotlin only.
• MvRx is Kotlin first and Kotlin only. • MvRx is Kotlin first and Kotlin only. • (很重要所以說三次) -
Why Kotlin Only? • MvRx is Kotlin first and Kotlin
only. By being Kotlin only, we could leverage several powerful language features for a cleaner API. If you are not familiar with Kotlin, in particular, data classes, and receiver types, please run through Kotlin Koans or other Kotlin tutorials before continuing with MvRx. -
Why Kotlin Only? • MvRx is Kotlin first and Kotlin
only. By being Kotlin only, we could leverage several powerful language features for a cleaner API. If you are not familiar with Kotlin, in particular, data classes, and receiver types, please run through Kotlin Koans or other Kotlin tutorials before continuing with MvRx. • 運⽤用的 Kotlin 語⾔言的特性,設計出更更精簡的 API,讓 MvRx 更更好⽤用 • Data classes • Receiver types -
MvRx 技術背景 • MvRx is built on top of the
following existing technologies and concepts: • Kotlin • Android Architecture Components • RxJava • React (conceptually) • Epoxy (optional but recommended) -
MvRx 技術背景 • MvRx is built on top of the
following existing technologies and concepts: • Kotlin • Android Architecture Components • RxJava • React (conceptually) • Epoxy (optional but recommended) 解決 Android Lifecycle 的問題 -
MvRx 技術背景 • MvRx is built on top of the
following existing technologies and concepts: • Kotlin • Android Architecture Components • RxJava • React (conceptually) • Epoxy (optional but recommended) 處理理 Async Requests,例例如網路路存取資料,資料庫 操作 -
MvRx 技術背景 • MvRx is built on top of the
following existing technologies and concepts: • Kotlin • Android Architecture Components • RxJava • React (conceptually) • Epoxy (optional but recommended) MvRx 的運作流程與 React 概念念相似 -
MvRx 技術背景 • MvRx is built on top of the
following existing technologies and concepts: • Kotlin • Android Architecture Components • RxJava • React (conceptually) • Epoxy (optional but recommended) Airbnb 的另⼀一個 library,RecyclerView 救星, 也可以與 MvRx 整合 -
Core Concepts • State • ViewModel • View • Async
先看 State, ViewModel, View 之間的關係 -
ViewModel • MvRxViewModel 為 Google ViewModel 的延伸 • MvRxViewModels ⽤用來來處理理所有的邏輯運算,⼀一個
ViewModel 會搭配 ⼀一個 State,可以在 ViewModel 中讀取、更更新、觀察 State -
ViewModel • MvRxViewModel 為 Google ViewModel 的延伸 • MvRxViewModels ⽤用來來處理理所有的邏輯運算,⼀一個
ViewModel 會搭配 ⼀一個 State,可以在 ViewModel 中讀取、更更新、觀察 State • 當 Configuration changes 時,Fragment、View、Activity 都會重新 產⽣生,⽽而 ViewModel 則不會 (Google ViewModel 的⾏行行為) -
ViewModel • 與 Google ViewModel 不同的是,MvRxViewModel 內操作的資料為 MvRxState (⽽而不是 LiveData),⽽而
View 只能呼叫 ViewModel 做事情並 觀察它的變化 (observable pattern) -
View • MvRxView 是 Interface,為 Android 的 LifecycleOwner 的延伸 •
使⽤用者必須在 View 實作 MvRxView (例例如 fragment), MvRxView 會 根據每次 State 的變化,呼叫 invalidate() function,通知 UI 更更新介⾯面 -
MvRxView State 有任何改變,通知 View Fragment • Fragment 呼叫 ViewModel 做事情
• State 有任何改變就會呼叫 MvRxView 的 invalidate() MvRxState MvRxViewModel -
State 有任何改變,通知 View Fragment • Fragments 之間也可以共⽤用同⼀一個 ViewModel (不同⾴頁⾯面共享資料) MvRxState
MvRxViewModel MvRxView -
Core Concepts • State • ViewModel • View • Async
專⾨門為處理理 async request 所建立的 class -
Async • Async 為 Kotlin 中的 sealed class,有四個 subclass •
Uninitialized • Loading • Success • Fail (其中包含⼀一個 error 欄欄位) • 在 MvRx 中,所有的 Async Request 都會⽤用 Async 表⽰示 -
Observable 整合 • MvRxViewModel 有實作 Observable 的 extension • 當我們對⼀一個
observable 呼叫 execute 時: 1.ViewModel 會⾃自動 subscribe 這個 observable 2.execute 會⾃自動把 observable 轉換成 Async 物件 3.Async 的狀狀態會變成 Loading、 Success 或 Fail fun <T> Observable<T>.execute(stateReducer: S.(Async<T>) -> S) -
Observable 整合 • ViewModel 會在 Lifecycle 結束時⾃自動捨棄 (dispose) subscription, 因此不⽤用擔⼼心
memory leak 的問題(不⽤用⼿手動 unsubscribe 啦!) • 當螢幕旋轉或是有任何 configuration changes,ViewModel 都會保留留 原來來的狀狀態,不⽤用擔⼼心 requesting 消失或狀狀態不⼀一致 -
⼩小結 • MvRx 定義出⼀一個標準的框架 • MvRx 幫你處理理 Android Lifecycle 的問題
• MvRx 幫你處理理 Configuration Changes 的問題 • MvRx 幫你解決 Fragments 共⽤用資料的問題 • MvRx 幫你處理理 Async Requests 的問題 • MvRx 讓你可以⽤用類似 React 的⽅方式開發 App -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 繼承 BaseMvRxFragment -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 定義 State -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 繼承 MvRxState -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 定義 ViewModel -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 繼承 MvRxViewModel -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 實作 MvRxView 的 Interface -
ViewModel 的建立⽅方式 • 有兩兩種建立⽅方式 • 透過 ViewModel 的 Constructor class
MyViewModel(initialState: MyState) : MvRxViewModel<MyState>(initialState) -
• 有兩兩種建立⽅方式 • 透過 ViewModel 的 Constructor • 透過 Factory
Method (如果有額外 dependency 需求) class MyViewModel(initialState: MyState, apiService: ApiService) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory<MyState, MyViewModel> { @JvmStatic override fun create(activity: FragmentActivity, state: MyState): MyViewModel { val apiService: ApiService by activity.inject() // access some DI framework. return MyViewModel(state, apiService) } } } ViewModel 的建立⽅方式 -
建立 ViewModel • fragmentViewModel:建立或讀取現有的 ViewModel,有效範圍為 Fragment (scoped to this Fragment)
• activityViewModel:建立或讀取現有的 ViewModel,有效範圍為 Activity (scoped to this Activity),通常使⽤用在多個 fragments 共享資 料 • existingViewModel:讀取現有的 ViewModel (Activity scope),若若沒 有已存在 ViewModel 則會回傳錯誤 -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 此 ViewModel 不需要額外參參數,只需 定義 Constructor -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 透過 kotlin delegates 的⽅方式產⽣生 viewModel -
ViewModel 讀取 State • 透過 「withState block」 • 語法原理理是什什麼? •
Function literals with receiver withState { state -> } -
ViewModel 更更新 State • 透過 「setState block」 setState { copy(title
= title) } • 由於 State 為 immutable object,要更更新 state,就必須重建⼀一個新的 object:利利⽤用 data class 的 copy method -
ViewModel Threading • 在 ViewModel 中存取 state 的 block 是皆為
background thread • 「withState block」會等待全部的 pending「setState block」都執⾏行行 完成後,才會執⾏行行,確保「withState block」拿到的是最新的 state -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { withState { state -> println("title: $state.title”) } setState { copy(title = “Android Taipei") } } } -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { withState { state -> println("title: $state.title”) } setState { copy(title = “Android Taipei") } } } block 內部皆為 background thread -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { withState { state -> println("title: $state.title”) } setState { copy(title = “Android Taipei") } } } // result: >> ?? -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { withState { state -> println("title: $state.title”) } setState { copy(title = “Android Taipei") } } } // result: >> "title: Android Taipei" -
ViewModel Threading • MvRx 其中⼀一個核⼼心概念念就是:thread-safe • 所有 non-view 的程式都可以在 background
thread 執⾏行行 • ⼤大⼤大降低使⽤用者⾃自⾏行行管理理 thread 的負擔 -
View 存取 State • 透過 「withState block」 withState(viewModel) { state
-> } • 表⽰示要存取特定 viewModel 中的 State -
View 存取 State • View 中的 「withState block」為 main thread
呼叫,呼叫時會直接回 傳⽬目前的 state 的 snapshot • 直接當作 function ⽤用即可 -
存取 State • withState block • setState block • 運⾏行行在
background thread • withState block • 運⾏行行在 main thread ViewModel View -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 呼叫 ViewModel 顯⽰示 Hello World -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } ⾃自⾏行行定義的 showTitle function -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 透過 setState 改變 state 的資料 -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 當 State 改變,會⾃自動觸發 invalidate() -
data class HelloWorldState(val title: String = "") : MvRxState class
HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState) { fun showTitle(title: String) { setState { copy(title = title) } } } class HelloWorldFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(HelloWorldViewModel::class) override fun onCreateView(inflater: LayoutInflater, ctn: ViewGroup?, bundle: Bundle?): View? { return inflater.inflate(R.layout.fragment_hello_world, ctn, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.showTitle("Hello World!") } override fun invalidate() { withState(viewModel) { titleTextView.text = it.title } } } 透過 withState block,讀取 state 並顯⽰示在畫⾯面上 -
範例例⼆二 - Sign In • 沒有輸入密碼,Client 端顯⽰示錯誤 • 輸入錯誤的密碼,顯⽰示 Server
的錯誤訊息 • 登入成功,關掉此⾴頁⾯面 • 登入時顯⽰示 Loading -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } State 與 ViewModel -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } 定義 async request -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } 由於我們需要從外部注入 ApiService,因此要利利 ⽤用 Factory Method 的⽅方式建立 ViewModel -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } 呼叫登入 API -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } fun signIn(emailOrPhone: String, password: String): Single<Account> -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } 執⾏行行 async request,須做兩兩件事: -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } 執⾏行行 async request,須做兩兩件事: 1. 呼叫 execute,⾃自動 subscribe -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } 執⾏行行 async request,須做兩兩件事: 1. 呼叫 execute,⾃自動 subscribe 2. 當結果回傳,更更新 state -
class SignInFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(SignInViewModel::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) logInButton.setOnClickListener { _ -> logIn() } viewModel.asyncSubscribe(SignInState::signInRequest, onSuccess = { showToast("Log in success.") popToRootFragment() }, onFail = { showErrorMessage(it) }) } private fun logIn() { viewModel.signIn(emailEditText.text.toString(), passwordEditText.text.toString()) } override fun invalidate() { withState(viewModel) { state -> if (state.signInRequest is Loading) { progressBar.visibility = View.VISIBLE } else { progressBar.visibility = View.GONE } } } } -
class SignInFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(SignInViewModel::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) logInButton.setOnClickListener { _ -> logIn() } viewModel.asyncSubscribe(SignInState::signInRequest, onSuccess = { showToast("Log in success.") popToRootFragment() }, onFail = { showErrorMessage(it) }) } private fun logIn() { viewModel.signIn(emailEditText.text.toString(), passwordEditText.text.toString()) } override fun invalidate() { withState(viewModel) { state -> if (state.signInRequest is Loading) { progressBar.visibility = View.VISIBLE } else { progressBar.visibility = View.GONE } } } } 呼叫 viewModel 執⾏行行登入 -
class SignInFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(SignInViewModel::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) logInButton.setOnClickListener { _ -> logIn() } viewModel.asyncSubscribe(SignInState::signInRequest, onSuccess = { showToast("Log in success.") popToRootFragment() }, onFail = { showErrorMessage(it) }) } private fun logIn() { viewModel.signIn(emailEditText.text.toString(), passwordEditText.text.toString()) } override fun invalidate() { withState(viewModel) { state -> if (state.signInRequest is Loading) { progressBar.visibility = View.VISIBLE } else { progressBar.visibility = View.GONE } } } } 根據 Async 的狀狀態,顯⽰示 Loading UI -
Subscribing to state manually • MvRx 提供三種額外⼿手動⽅方式觀察 state • subscribe
- 觀察整個 state • selectSubscribe - 只觀察 state 中的特定 property • asyncSubscribe - 只觀察 Async 類別的 property -
class SignInFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(SignInViewModel::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) logInButton.setOnClickListener { _ -> logIn() } viewModel.asyncSubscribe(SignInState::signInRequest, onSuccess = { showToast("Log in success.") popToRootFragment() }, onFail = { showErrorMessage(it) }) } private fun logIn() { viewModel.signIn(emailEditText.text.toString(), passwordEditText.text.toString()) } override fun invalidate() { withState(viewModel) { state -> if (state.signInRequest is Loading) { progressBar.visibility = View.VISIBLE } else { progressBar.visibility = View.GONE } } } } ⼿手動 subscribe -
class SignInFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(SignInViewModel::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) logInButton.setOnClickListener { _ -> logIn() } viewModel.asyncSubscribe(SignInState::signInRequest, onSuccess = { showToast("Log in success.") popToRootFragment() }, onFail = { showErrorMessage(it) }) } private fun logIn() { viewModel.signIn(emailEditText.text.toString(), passwordEditText.text.toString()) } override fun invalidate() { withState(viewModel) { state -> if (state.signInRequest is Loading) { progressBar.visibility = View.VISIBLE } else { progressBar.visibility = View.GONE } } } } 指定要觀察 state 中哪⼀一個 property -
class SignInFragment : BaseMvRxFragment() { private val viewModel by fragmentViewModel(SignInViewModel::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) logInButton.setOnClickListener { _ -> logIn() } viewModel.asyncSubscribe(SignInState::signInRequest, onSuccess = { showToast("Log in success.") popToRootFragment() }, onFail = { showErrorMessage(it) }) } private fun logIn() { viewModel.signIn(emailEditText.text.toString(), passwordEditText.text.toString()) } override fun invalidate() { withState(viewModel) { state -> if (state.signInRequest is Loading) { progressBar.visibility = View.VISIBLE } else { progressBar.visibility = View.GONE } } } } asyncSubscribe 提供 onSuccess 與 onFail 兩兩種 callbacks -
Test ViewModel • ViewMode 是可測試的 • ⽬目前 MvRx (v0.5.0) 測試環境設定稍嫌複雜,issue
中有提到之後會改進 (請參參考 MvRx project 中的 BaseTest class 設定) • 需搭配 Robolectric -
data class SignInState(val signInRequest: Async<Account> = Uninitialized) : MvRxState class
SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel<SignInState>(initialState) { companion object : MvRxViewModelFactory<SignInState> { @JvmStatic override fun create(activity: FragmentActivity, state: SignInState): SignInViewModel { val apiService:ApiService = activity.inject() // access some DI framework. return SignInViewModel(state, apiService) } } fun signIn(email: String, password: String) { Single.fromCallable { if (email.isEmpty() || password.isEmpty()) { throw Throwable("empty email or password") } }.flatMap { apiService.signIn(email, password) }.execute { copy(signInRequest = it) } } } -
class ExampleUnitTest : BaseTest() { private lateinit var owner: TestLifecycleOwner
@Before fun setup() { owner = TestLifecycleOwner() owner.lifecycle.markState(Lifecycle.State.RESUMED) } @Test fun signIn_emptyPassword() { val signInViewModel = SignInViewModel(SignInState(), ApiService()) signInViewModel.asyncSubscribe(owner, SignInState::signInRequest, onSuccess = { assertTrue(false) }, onFail = { assertTrue(true) assertEquals("empty email or password", it.localizedMessage) }) signInViewModel.signIn("[email protected]", "") } } -
class ExampleUnitTest : BaseTest() { private lateinit var owner: TestLifecycleOwner
@Before fun setup() { owner = TestLifecycleOwner() owner.lifecycle.markState(Lifecycle.State.RESUMED) } @Test fun signIn_emptyPassword() { val signInViewModel = SignInViewModel(SignInState(), ApiService()) signInViewModel.asyncSubscribe(owner, SignInState::signInRequest, onSuccess = { assertTrue(false) }, onFail = { assertTrue(true) assertEquals("empty email or password", it.localizedMessage) }) signInViewModel.signIn("[email protected]", "") } } 定義測試專⽤用的 LifecycleOwner -
class ExampleUnitTest : BaseTest() { private lateinit var owner: TestLifecycleOwner
@Before fun setup() { owner = TestLifecycleOwner() owner.lifecycle.markState(Lifecycle.State.RESUMED) } @Test fun signIn_emptyPassword() { val signInViewModel = SignInViewModel(SignInState(), ApiService()) signInViewModel.asyncSubscribe(owner, SignInState::signInRequest, onSuccess = { assertTrue(false) }, onFail = { assertTrue(true) assertEquals("empty email or password", it.localizedMessage) }) signInViewModel.signIn("[email protected]", "") } } -
class ExampleUnitTest : BaseTest() { private lateinit var owner: TestLifecycleOwner
@Before fun setup() { owner = TestLifecycleOwner() owner.lifecycle.markState(Lifecycle.State.RESUMED) } @Test fun signIn_emptyPassword() { val signInViewModel = SignInViewModel(SignInState(), ApiService()) signInViewModel.asyncSubscribe(owner, SignInState::signInRequest, onSuccess = { assertTrue(false) }, onFail = { assertTrue(true) assertEquals("empty email or password", it.localizedMessage) }) signInViewModel.signIn("[email protected]", "") } } -
⼩小結 • MvRx 定義出⼀一個標準的框架 • MvRx 幫你處理理 Android Lifecycle 的問題
• MvRx 幫你處理理 configuration changes 的問題 • MvRx 幫你解決 Fragments 共⽤用資料的問題 • MvRx 幫你處理理 Async Requests 的問題 • MvRx 讓你可以⽤用類似 React 的⽅方式開發 App • MvRx 為 Thread-safe • MvRx 是可測試的 -
Dependency Injection • 可以跟什什麼 DI library 整合? • Koin •
Dagger 2 (可搭配 AssistedInject) -
Kotlin coroutines • ⽬目前不⽀支援 coroutines,只⽀支援 RxJava • 若若有興趣,可⾃自⾏行行把 coroutines 包裝成
Async • 有討論中的 issue: https://github.com/airbnb/MvRx/issues/79 • 指⽇日可待? -
Epoxy • Epoxy 為 Airbnb 另⼀一個開源 library,⽤用來來建立複雜的 RecyclerView Layout •
可與 MvRx 整合,當 state 更更新時,只會更更新有變化的 element • 寫起來來更更 Reactive -
優點 • 全原⽣生開發,與你的 code 100% 相容 • 導入快速 (可只導入單⼀一⾴頁⾯面) •
節省開發時間 • 按照框架可以簡單寫出⾼高品質的程式碼 -
缺點 • 學習⾨門檻稍⾼高 • 以 Airbnb 的訴求為主,提需求不⼀一定會被接受 (例例如現在只接受 Fragment )
• Unit Test 設定複雜,待⽇日後改進 • 正在開發中,API 還會變動 -
要怎麼開始導入? • Convert Java to Kotlin • 修改 Base Activity
• 修改 Base Fragment • 建立 Base ViewModel -
Reference • Introducing MvRx: Android on Autopilot https://medium.com/airbnb-engineering/introducing-mvrx- android-on-autopilot-552bca86bd0a •
MvRx Github Page https://github.com/airbnb/MvRx • Work with dagger https://github.com/chrisbanes/tivi/pull/214 -
Reference • Kotlin Function Literals with Receiver https://kotlinexpertise.com/function-literals-with-receiver/ • AssistedInject
https://github.com/google/guice/wiki/AssistedInject • Epoxy https://github.com/airbnb/epoxy