Slide 1

Slide 1 text

MvRx 介紹 黃千碩 (Kros) 打⼯工趣 Mobile App Developer

Slide 2

Slide 2 text

Outline • 什什麼是 MvRx (唸作 mavericks) • Core Concepts • 實作細節 • Testing • 其他

Slide 3

Slide 3 text

什什麼是 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 的框架,可以⼤大幅減少撰寫的程式碼

Slide 4

Slide 4 text

幾個開發上的問題 • 複雜的 Android Lifecycle 問題 • 在 onSaveInstanceState 的處理理邏輯,與 view state 狀狀態儲存問題 • 在網路路或資料庫的非同步 (asynchronous requests) 呼叫時, onSuccess, onFailure 的處理理,thread 切換等問題 • Android 程式中預設都是 main thread,能否更更簡單的做 Threading 切 換,把複雜的計算放到 background thread

Slide 5

Slide 5 text

什什麼是 MvRx • 為了了解決這些問題,Airbnb ⾃自⾏行行開發了了⼀一套框架 • ⽬目的是讓使⽤用者可以按照固定的架構,寫出⾼高品質、好維護、效能佳、 Bug 少的 Android App • 已經⼤大幅在 Airbnb 的產品上採⽤用

Slide 6

Slide 6 text

什什麼是 MvRx • MvRx is Kotlin first and Kotlin only. • MvRx is Kotlin first and Kotlin only. • MvRx is Kotlin first and Kotlin only. • (很重要所以說三次)

Slide 7

Slide 7 text

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.

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

MvRx 技術背景 • MvRx is built on top of the following existing technologies and concepts: • Kotlin • Android Architecture Components • RxJava • React (conceptually) • Epoxy (optional but recommended)

Slide 10

Slide 10 text

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 的問題

Slide 11

Slide 11 text

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,例例如網路路存取資料,資料庫 操作

Slide 12

Slide 12 text

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 概念念相似

Slide 13

Slide 13 text

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 整合

Slide 14

Slide 14 text

Core Concepts • State • ViewModel • View • Async

Slide 15

Slide 15 text

Core Concepts • State • ViewModel • View • Async 先看 State, ViewModel, View 之間的關係

Slide 16

Slide 16 text

State • MvRxState ⽤用來來儲存每個畫⾯面所需要的資料,為不可修改的物件 (immutable object),單純儲存資料,沒有邏輯 • 本⾝身為 Data class

Slide 17

Slide 17 text

ViewModel • MvRxViewModel 為 Google ViewModel 的延伸

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

ViewModel • MvRxViewModel 為 Google ViewModel 的延伸 • MvRxViewModels ⽤用來來處理理所有的邏輯運算,⼀一個 ViewModel 會搭配 ⼀一個 State,可以在 ViewModel 中讀取、更更新、觀察 State • 當 Configuration changes 時,Fragment、View、Activity 都會重新 產⽣生,⽽而 ViewModel 則不會 (Google ViewModel 的⾏行行為)

Slide 20

Slide 20 text

ViewModel

Slide 21

Slide 21 text

ViewModel • 與 Google ViewModel 不同的是,MvRxViewModel 內操作的資料為 MvRxState (⽽而不是 LiveData),⽽而 View 只能呼叫 ViewModel 做事情並 觀察它的變化 (observable pattern)

Slide 22

Slide 22 text

View • MvRxView 是 Interface,為 Android 的 LifecycleOwner 的延伸 • 使⽤用者必須在 View 實作 MvRxView (例例如 fragment), MvRxView 會 根據每次 State 的變化,呼叫 invalidate() function,通知 UI 更更新介⾯面

Slide 23

Slide 23 text

State, ViewModel, View ⽰示意圖

Slide 24

Slide 24 text

Fragment • 在 Fragment 中

Slide 25

Slide 25 text

MvRxState Fragment • 定義此⾴頁⾯面所需要的 Data (就是 State)

Slide 26

Slide 26 text

MvRxViewModel Fragment • 定義可以存取 State 的 ViewModel MvRxState

Slide 27

Slide 27 text

MvRxView Fragment • Fragment 實作 MvRxView Interface MvRxViewModel MvRxState

Slide 28

Slide 28 text

MvRxView State 有任何改變,通知 View Fragment • Fragment 呼叫 ViewModel 做事情 • State 有任何改變就會呼叫 MvRxView 的 invalidate() MvRxState MvRxViewModel

Slide 29

Slide 29 text

State 有任何改變,通知 View Fragment • Fragments 之間也可以共⽤用同⼀一個 ViewModel (不同⾴頁⾯面共享資料) MvRxState MvRxViewModel MvRxView

Slide 30

Slide 30 text

Core Concepts • State • ViewModel • View • Async 專⾨門為處理理 async request 所建立的 class

Slide 31

Slide 31 text

Async • Async 為 Kotlin 中的 sealed class,有四個 subclass • Uninitialized • Loading • Success • Fail (其中包含⼀一個 error 欄欄位) • 在 MvRx 中,所有的 Async Request 都會⽤用 Async 表⽰示

Slide 32

Slide 32 text

Observable 整合 • MvRxViewModel 有實作 Observable 的 extension • 當我們對⼀一個 observable 呼叫 execute 時: 1.ViewModel 會⾃自動 subscribe 這個 observable 2.execute 會⾃自動把 observable 轉換成 Async 物件 3.Async 的狀狀態會變成 Loading、 Success 或 Fail fun Observable.execute(stateReducer: S.(Async) -> S)

Slide 33

Slide 33 text

Observable 整合 • ViewModel 會在 Lifecycle 結束時⾃自動捨棄 (dispose) subscription, 因此不⽤用擔⼼心 memory leak 的問題(不⽤用⼿手動 unsubscribe 啦!) • 當螢幕旋轉或是有任何 configuration changes,ViewModel 都會保留留 原來來的狀狀態,不⽤用擔⼼心 requesting 消失或狀狀態不⼀一致

Slide 34

Slide 34 text

⼩小結 • MvRx 定義出⼀一個標準的框架 • MvRx 幫你處理理 Android Lifecycle 的問題 • MvRx 幫你處理理 Configuration Changes 的問題 • MvRx 幫你解決 Fragments 共⽤用資料的問題 • MvRx 幫你處理理 Async Requests 的問題 • MvRx 讓你可以⽤用類似 React 的⽅方式開發 App

Slide 35

Slide 35 text

實作細節

Slide 36

Slide 36 text

範例例⼀一

Slide 37

Slide 37 text

Hello World

Slide 38

Slide 38 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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 } } }

Slide 39

Slide 39 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 40

Slide 40 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 41

Slide 41 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 42

Slide 42 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 43

Slide 43 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 44

Slide 44 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 45

Slide 45 text

ViewModel 細節 • ViewModel 建立⽅方式 • 存取 State • 更更新 State

Slide 46

Slide 46 text

ViewModel 細節 • ViewModel 建立⽅方式 • 存取 State • 更更新 State

Slide 47

Slide 47 text

建立 ViewModel • 設定 ViewModel 建立⽅方式 • 透過 Kotlin Delegates ⽅方式建立

Slide 48

Slide 48 text

ViewModel 的建立⽅方式 • 有兩兩種建立⽅方式 • 透過 ViewModel 的 Constructor class MyViewModel(initialState: MyState) : MvRxViewModel(initialState)

Slide 49

Slide 49 text

• 有兩兩種建立⽅方式 • 透過 ViewModel 的 Constructor • 透過 Factory Method (如果有額外 dependency 需求) class MyViewModel(initialState: MyState, apiService: ApiService) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @JvmStatic override fun create(activity: FragmentActivity, state: MyState): MyViewModel { val apiService: ApiService by activity.inject() // access some DI framework. return MyViewModel(state, apiService) } } } ViewModel 的建立⽅方式

Slide 50

Slide 50 text

建立 ViewModel • MvRx 提供的 extension method 建立 ViewModel

Slide 51

Slide 51 text

建立 ViewModel • fragmentViewModel:建立或讀取現有的 ViewModel,有效範圍為 Fragment (scoped to this Fragment) • activityViewModel:建立或讀取現有的 ViewModel,有效範圍為 Activity (scoped to this Activity),通常使⽤用在多個 fragments 共享資 料 • existingViewModel:讀取現有的 ViewModel (Activity scope),若若沒 有已存在 ViewModel 則會回傳錯誤

Slide 52

Slide 52 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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 } } }

Slide 53

Slide 53 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 54

Slide 54 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 55

Slide 55 text

ViewModel 細節 • ViewModel 建立⽅方式 • 存取 State • 更更新 State

Slide 56

Slide 56 text

ViewModel 讀取 State • 透過 「withState block」 • 語法原理理是什什麼? withState { state -> }

Slide 57

Slide 57 text

ViewModel 讀取 State • 透過 「withState block」 • 語法原理理是什什麼? • Function literals with receiver withState { state -> }

Slide 58

Slide 58 text

ViewModel 更更新 State • 透過 「setState block」 setState { copy(title = title) }

Slide 59

Slide 59 text

ViewModel 更更新 State • 透過 「setState block」 setState { copy(title = title) } • 由於 State 為 immutable object,要更更新 state,就必須重建⼀一個新的 object:利利⽤用 data class 的 copy method

Slide 60

Slide 60 text

ViewModel Threading • 在 ViewModel 中存取 state 的 block 是皆為 background thread • 「withState block」會等待全部的 pending「setState block」都執⾏行行 完成後,才會執⾏行行,確保「withState block」拿到的是最新的 state

Slide 61

Slide 61 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(initialState) { fun showTitle(title: String) { withState { state -> println("title: $state.title”) } setState { copy(title = “Android Taipei") } } }

Slide 62

Slide 62 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(initialState) { fun showTitle(title: String) { withState { state -> println("title: $state.title”) } setState { copy(title = “Android Taipei") } } } block 內部皆為 background thread

Slide 63

Slide 63 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(initialState) { fun showTitle(title: String) { withState { state -> println("title: $state.title”) } setState { copy(title = “Android Taipei") } } } // result: >> ??

Slide 64

Slide 64 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(initialState) { fun showTitle(title: String) { withState { state -> println("title: $state.title”) } setState { copy(title = “Android Taipei") } } } // result: >> "title: Android Taipei"

Slide 65

Slide 65 text

ViewModel Threading • MvRx 其中⼀一個核⼼心概念念就是:thread-safe • 所有 non-view 的程式都可以在 background thread 執⾏行行 • ⼤大⼤大降低使⽤用者⾃自⾏行行管理理 thread 的負擔

Slide 66

Slide 66 text

View 存取 State • 透過 「withState block」 withState(viewModel) { state -> } • 表⽰示要存取特定 viewModel 中的 State

Slide 67

Slide 67 text

View 存取 State • View 中的 「withState block」為 main thread 呼叫,呼叫時會直接回 傳⽬目前的 state 的 snapshot • 直接當作 function ⽤用即可

Slide 68

Slide 68 text

存取 State • withState block • setState block • 運⾏行行在 background thread • withState block • 運⾏行行在 main thread ViewModel View

Slide 69

Slide 69 text

再看⼀一次剛剛的範例例

Slide 70

Slide 70 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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 } } }

Slide 71

Slide 71 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 72

Slide 72 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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

Slide 73

Slide 73 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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 的資料

Slide 74

Slide 74 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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()

Slide 75

Slide 75 text

data class HelloWorldState(val title: String = "") : MvRxState class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel(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 並顯⽰示在畫⾯面上

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

範例例⼆二

Slide 78

Slide 78 text

範例例⼆二 - Sign In • 沒有輸入密碼,Client 端顯⽰示錯誤 • 輸入錯誤的密碼,顯⽰示 Server 的錯誤訊息 • 登入成功,關掉此⾴頁⾯面 • 登入時顯⽰示 Loading

Slide 79

Slide 79 text

Sign In Fragment

Slide 80

Slide 80 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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

Slide 81

Slide 81 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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

Slide 82

Slide 82 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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

Slide 83

Slide 83 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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

Slide 84

Slide 84 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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

Slide 85

Slide 85 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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,須做兩兩件事:

Slide 86

Slide 86 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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

Slide 87

Slide 87 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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

Slide 88

Slide 88 text

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 } } } }

Slide 89

Slide 89 text

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 執⾏行行登入

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

• 除了了 invalidate() 之外,有其他⽅方法可以觀察 state 嗎? Subscribing to state manually

Slide 92

Slide 92 text

Subscribing to state manually • MvRx 提供三種額外⼿手動⽅方式觀察 state • subscribe - 觀察整個 state • selectSubscribe - 只觀察 state 中的特定 property • asyncSubscribe - 只觀察 Async 類別的 property

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

Testing

Slide 97

Slide 97 text

Test ViewModel • ViewMode 是可測試的 • ⽬目前 MvRx (v0.5.0) 測試環境設定稍嫌複雜,issue 中有提到之後會改進
 (請參參考 MvRx project 中的 BaseTest class 設定) • 需搭配 Robolectric

Slide 98

Slide 98 text

Test ViewModel • 範例例:測試輸入空⽩白密碼 • Test case:當使⽤用者輸入空⽩白密碼,系統會回傳錯誤,並攜帶錯誤訊息
 「empty email or password」

Slide 99

Slide 99 text

data class SignInState(val signInRequest: Async = Uninitialized) : MvRxState class SignInViewModel( initialState: SignInState, private val apiService: ApiService ) : MvRxViewModel(initialState) { companion object : MvRxViewModelFactory { @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) } } }

Slide 100

Slide 100 text

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]", "") } }

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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]", "") } }

Slide 103

Slide 103 text

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]", "") } }

Slide 104

Slide 104 text

⼩小結 • MvRx 定義出⼀一個標準的框架 • MvRx 幫你處理理 Android Lifecycle 的問題 • MvRx 幫你處理理 configuration changes 的問題 • MvRx 幫你解決 Fragments 共⽤用資料的問題 • MvRx 幫你處理理 Async Requests 的問題 • MvRx 讓你可以⽤用類似 React 的⽅方式開發 App • MvRx 為 Thread-safe • MvRx 是可測試的

Slide 105

Slide 105 text

其他

Slide 106

Slide 106 text

Dependency Injection • 可以跟什什麼 DI library 整合? • Koin • Dagger 2 (可搭配 AssistedInject)

Slide 107

Slide 107 text

Kotlin coroutines • ⽬目前不⽀支援 coroutines,只⽀支援 RxJava • 若若有興趣,可⾃自⾏行行把 coroutines 包裝成 Async • 有討論中的 issue:
 https://github.com/airbnb/MvRx/issues/79 • 指⽇日可待?

Slide 108

Slide 108 text

Epoxy • Epoxy 為 Airbnb 另⼀一個開源 library,⽤用來來建立複雜的 RecyclerView Layout • 可與 MvRx 整合,當 state 更更新時,只會更更新有變化的 element • 寫起來來更更 Reactive

Slide 109

Slide 109 text

優點 • 全原⽣生開發,與你的 code 100% 相容 • 導入快速 (可只導入單⼀一⾴頁⾯面) • 節省開發時間 • 按照框架可以簡單寫出⾼高品質的程式碼

Slide 110

Slide 110 text

缺點 • 學習⾨門檻稍⾼高 • 以 Airbnb 的訴求為主,提需求不⼀一定會被接受
 (例例如現在只接受 Fragment ) • Unit Test 設定複雜,待⽇日後改進 • 正在開發中,API 還會變動

Slide 111

Slide 111 text

要怎麼開始導入? • Convert Java to Kotlin • 修改 Base Activity • 修改 Base Fragment • 建立 Base ViewModel

Slide 112

Slide 112 text

Thank you!

Slide 113

Slide 113 text

Sample Code • https://github.com/ch8908/MvRxSample

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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