Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android MvRx Framework 介紹

Android MvRx Framework 介紹

2018/10/24 Android Taipei 分享

介紹如何使用 Airbnb 的 MvRx Framework

用 React, Redux like 的方式開發 App

Chien Shuo (Kros)

October 24, 2018
Tweet

More Decks by Chien Shuo (Kros)

Other Decks in Programming

Transcript

  1. 什什麼是 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 的框架,可以⼤大幅減少撰寫的程式碼
  2. 幾個開發上的問題 • 複雜的 Android Lifecycle 問題 • 在 onSaveInstanceState 的處理理邏輯,與

    view state 狀狀態儲存問題 • 在網路路或資料庫的非同步 (asynchronous requests) 呼叫時, onSuccess, onFailure 的處理理,thread 切換等問題 • Android 程式中預設都是 main thread,能否更更簡單的做 Threading 切 換,把複雜的計算放到 background thread
  3. 什什麼是 MvRx • MvRx is Kotlin first and Kotlin only.

    • MvRx is Kotlin first and Kotlin only. • MvRx is Kotlin first and Kotlin only. • (很重要所以說三次)
  4. 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.
  5. 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
  6. MvRx 技術背景 • MvRx is built on top of the

    following existing technologies and concepts: • Kotlin • Android Architecture Components • RxJava • React (conceptually) • Epoxy (optional but recommended)
  7. 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 的問題
  8. 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,例例如網路路存取資料,資料庫 操作
  9. 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 概念念相似
  10. 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 整合
  11. Core Concepts • State • ViewModel • View • Async

    先看 State, ViewModel, View 之間的關係
  12. ViewModel • MvRxViewModel 為 Google ViewModel 的延伸 • MvRxViewModels ⽤用來來處理理所有的邏輯運算,⼀一個

    ViewModel 會搭配 ⼀一個 State,可以在 ViewModel 中讀取、更更新、觀察 State
  13. ViewModel • MvRxViewModel 為 Google ViewModel 的延伸 • MvRxViewModels ⽤用來來處理理所有的邏輯運算,⼀一個

    ViewModel 會搭配 ⼀一個 State,可以在 ViewModel 中讀取、更更新、觀察 State • 當 Configuration changes 時,Fragment、View、Activity 都會重新 產⽣生,⽽而 ViewModel 則不會 (Google ViewModel 的⾏行行為)
  14. View • MvRxView 是 Interface,為 Android 的 LifecycleOwner 的延伸 •

    使⽤用者必須在 View 實作 MvRxView (例例如 fragment), MvRxView 會 根據每次 State 的變化,呼叫 invalidate() function,通知 UI 更更新介⾯面
  15. MvRxView State 有任何改變,通知 View Fragment • Fragment 呼叫 ViewModel 做事情

    • State 有任何改變就會呼叫 MvRxView 的 invalidate() MvRxState MvRxViewModel
  16. Core Concepts • State • ViewModel • View • Async

    專⾨門為處理理 async request 所建立的 class
  17. Async • Async 為 Kotlin 中的 sealed class,有四個 subclass •

    Uninitialized • Loading • Success • Fail (其中包含⼀一個 error 欄欄位) • 在 MvRx 中,所有的 Async Request 都會⽤用 Async 表⽰示
  18. 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)
  19. Observable 整合 • ViewModel 會在 Lifecycle 結束時⾃自動捨棄 (dispose) subscription, 因此不⽤用擔⼼心

    memory leak 的問題(不⽤用⼿手動 unsubscribe 啦!) • 當螢幕旋轉或是有任何 configuration changes,ViewModel 都會保留留 原來來的狀狀態,不⽤用擔⼼心 requesting 消失或狀狀態不⼀一致
  20. ⼩小結 • MvRx 定義出⼀一個標準的框架 • MvRx 幫你處理理 Android Lifecycle 的問題

    • MvRx 幫你處理理 Configuration Changes 的問題 • MvRx 幫你解決 Fragments 共⽤用資料的問題 • MvRx 幫你處理理 Async Requests 的問題 • MvRx 讓你可以⽤用類似 React 的⽅方式開發 App
  21. 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 } } }
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. ViewModel 的建立⽅方式 • 有兩兩種建立⽅方式 • 透過 ViewModel 的 Constructor class

    MyViewModel(initialState: MyState) : MvRxViewModel<MyState>(initialState)
  29. • 有兩兩種建立⽅方式 • 透過 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 的建立⽅方式
  30. 建立 ViewModel • fragmentViewModel:建立或讀取現有的 ViewModel,有效範圍為 Fragment (scoped to this Fragment)

    • activityViewModel:建立或讀取現有的 ViewModel,有效範圍為 Activity (scoped to this Activity),通常使⽤用在多個 fragments 共享資 料 • existingViewModel:讀取現有的 ViewModel (Activity scope),若若沒 有已存在 ViewModel 則會回傳錯誤
  31. 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 } } }
  32. 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
  33. 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
  34. ViewModel 更更新 State • 透過 「setState block」 setState { copy(title

    = title) } • 由於 State 為 immutable object,要更更新 state,就必須重建⼀一個新的 object:利利⽤用 data class 的 copy method
  35. ViewModel Threading • 在 ViewModel 中存取 state 的 block 是皆為

    background thread • 「withState block」會等待全部的 pending「setState block」都執⾏行行 完成後,才會執⾏行行,確保「withState block」拿到的是最新的 state
  36. 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") } } }
  37. 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
  38. 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: >> ??
  39. 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"
  40. ViewModel Threading • MvRx 其中⼀一個核⼼心概念念就是:thread-safe • 所有 non-view 的程式都可以在 background

    thread 執⾏行行 • ⼤大⼤大降低使⽤用者⾃自⾏行行管理理 thread 的負擔
  41. View 存取 State • 透過 「withState block」 withState(viewModel) { state

    -> } • 表⽰示要存取特定 viewModel 中的 State
  42. View 存取 State • View 中的 「withState block」為 main thread

    呼叫,呼叫時會直接回 傳⽬目前的 state 的 snapshot • 直接當作 function ⽤用即可
  43. 存取 State • withState block • setState block • 運⾏行行在

    background thread • withState block • 運⾏行行在 main thread ViewModel View
  44. 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 } } }
  45. 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
  46. 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
  47. 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 的資料
  48. 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()
  49. 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 並顯⽰示在畫⾯面上
  50. 範例例⼆二 - Sign In • 沒有輸入密碼,Client 端顯⽰示錯誤 • 輸入錯誤的密碼,顯⽰示 Server

    的錯誤訊息 • 登入成功,關掉此⾴頁⾯面 • 登入時顯⽰示 Loading
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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>
  56. 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,須做兩兩件事:
  57. 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
  58. 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
  59. 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 } } } }
  60. 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 執⾏行行登入
  61. 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
  62. Subscribing to state manually • MvRx 提供三種額外⼿手動⽅方式觀察 state • subscribe

    - 觀察整個 state • selectSubscribe - 只觀察 state 中的特定 property • asyncSubscribe - 只觀察 Async 類別的 property
  63. 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
  64. 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
  65. 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
  66. Test ViewModel • ViewMode 是可測試的 • ⽬目前 MvRx (v0.5.0) 測試環境設定稍嫌複雜,issue

    中有提到之後會改進
 (請參參考 MvRx project 中的 BaseTest class 設定) • 需搭配 Robolectric
  67. 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) } } }
  68. 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]", "") } }
  69. 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
  70. 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]", "") } }
  71. 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]", "") } }
  72. ⼩小結 • MvRx 定義出⼀一個標準的框架 • MvRx 幫你處理理 Android Lifecycle 的問題

    • MvRx 幫你處理理 configuration changes 的問題 • MvRx 幫你解決 Fragments 共⽤用資料的問題 • MvRx 幫你處理理 Async Requests 的問題 • MvRx 讓你可以⽤用類似 React 的⽅方式開發 App • MvRx 為 Thread-safe • MvRx 是可測試的
  73. Kotlin coroutines • ⽬目前不⽀支援 coroutines,只⽀支援 RxJava • 若若有興趣,可⾃自⾏行行把 coroutines 包裝成

    Async • 有討論中的 issue:
 https://github.com/airbnb/MvRx/issues/79 • 指⽇日可待?
  74. Epoxy • Epoxy 為 Airbnb 另⼀一個開源 library,⽤用來來建立複雜的 RecyclerView Layout •

    可與 MvRx 整合,當 state 更更新時,只會更更新有變化的 element • 寫起來來更更 Reactive
  75. 優點 • 全原⽣生開發,與你的 code 100% 相容 • 導入快速 (可只導入單⼀一⾴頁⾯面) •

    節省開發時間 • 按照框架可以簡單寫出⾼高品質的程式碼
  76. 要怎麼開始導入? • Convert Java to Kotlin • 修改 Base Activity

    • 修改 Base Fragment • 建立 Base ViewModel
  77. 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
  78. 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