SlideShare a Scribd company logo
MvRx 介紹
黃千碩 (Kros)
打⼯工趣
Mobile App Developer
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
Core Concepts
• State
• ViewModel
• View
• Async
先看 State, ViewModel, View 之間的關係
State
• MvRxState ⽤用來來儲存每個畫⾯面所需要的資料,為不可修改的物件
(immutable object),單純儲存資料,沒有邏輯
• 本⾝身為 Data class
ViewModel
• MvRxViewModel 為 Google ViewModel 的延伸
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
ViewModel
• 與 Google ViewModel 不同的是,MvRxViewModel 內操作的資料為
MvRxState (⽽而不是 LiveData),⽽而 View 只能呼叫 ViewModel 做事情並
觀察它的變化 (observable pattern)
View
• MvRxView 是 Interface,為 Android 的 LifecycleOwner 的延伸
• 使⽤用者必須在 View 實作 MvRxView (例例如 fragment), MvRxView 會
根據每次 State 的變化,呼叫 invalidate() function,通知 UI 更更新介⾯面
State, ViewModel, View
⽰示意圖
Fragment
• 在 Fragment 中
MvRxState
Fragment
• 定義此⾴頁⾯面所需要的 Data (就是 State)
MvRxViewModel
Fragment
• 定義可以存取 State 的 ViewModel
MvRxState
MvRxView
Fragment
• Fragment 實作 MvRxView Interface
MvRxViewModel
MvRxState
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
實作細節
範例例⼀一
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
}
}
}
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 建立⽅方式
• 存取 State
• 更更新 State
ViewModel 細節
• ViewModel 建立⽅方式
• 存取 State
• 更更新 State
建立 ViewModel
• 設定 ViewModel 建立⽅方式
• 透過 Kotlin Delegates ⽅方式建立
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
• MvRx 提供的 extension method 建立 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 細節
• ViewModel 建立⽅方式
• 存取 State
• 更更新 State
ViewModel 讀取 State
• 透過 「withState block」
• 語法原理理是什什麼?
withState { state -> }
ViewModel 讀取 State
• 透過 「withState block」
• 語法原理理是什什麼?
• Function literals with receiver
withState { state -> }
ViewModel 更更新 State
• 透過 「setState block」
setState { copy(title = title) }
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
並顯⽰示在畫⾯面上
Android MvRx Framework 介紹
範例例⼆二
範例例⼆二 - Sign In
• 沒有輸入密碼,Client 端顯⽰示錯誤
• 輸入錯誤的密碼,顯⽰示 Server 的錯誤訊息
• 登入成功,關掉此⾴頁⾯面
• 登入時顯⽰示 Loading
Sign In Fragment
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
• 除了了 invalidate() 之外,有其他⽅方法可以觀察 state 嗎?
Subscribing to state manually
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
Testing
Test ViewModel
• ViewMode 是可測試的
• ⽬目前 MvRx (v0.5.0) 測試環境設定稍嫌複雜,issue 中有提到之後會改進

(請參參考 MvRx project 中的 BaseTest class 設定)
• 需搭配 Robolectric
Test ViewModel
• 範例例:測試輸入空⽩白密碼
• Test case:當使⽤用者輸入空⽩白密碼,系統會回傳錯誤,並攜帶錯誤訊息

「empty email or password」
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("test@mail.com", "")
}
}
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("test@mail.com", "")
}
}
定義測試專⽤用的 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("test@mail.com", "")
}
}
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("test@mail.com", "")
}
}
⼩小結
• 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
Thank you!
Sample Code
• https://github.com/ch8908/MvRxSample
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

More Related Content

PPTX
Google app engine
PDF
Events and Listeners in Android
PPTX
Android Architecture.pptx
PPTX
Hidden surface removal algorithm
PPTX
Android+init+process
PPTX
Lecture 5&6 corporate architecture
PDF
4. THREE DIMENSIONAL DISPLAY METHODS
PDF
Mobile operating systems
Google app engine
Events and Listeners in Android
Android Architecture.pptx
Hidden surface removal algorithm
Android+init+process
Lecture 5&6 corporate architecture
4. THREE DIMENSIONAL DISPLAY METHODS
Mobile operating systems

What's hot (20)

PDF
Android Programming Basics
PPT
.net framework
PPTX
Window to Viewport Transformation in Computer Graphics with.pptx
PPTX
Android styles and themes
PPTX
PPT
ASE Consulting Vee Model
PDF
How to chtmultiregionfoam
PPTX
Android PPT Presentation 2018
PPTX
flexray technology in modern cars
PDF
HTML5 Apps on AGL Platform with the Web Application Manager (Automotive Grade...
PPTX
PPTX
Android Layout
PPTX
Projection In Computer Graphics
PDF
Kernel Recipes 2015: Representing device-tree peripherals in ACPI
PDF
Solutions manual for guide to sql 9th edition by pratt
PDF
Board support package_on_linux
PDF
UIStackView – Tom Bowden – Dec 2015 – Eventacular Inc
PPTX
Introduction to android
PPTX
iOS Architecture
PPT
Introduction to VB.net
Android Programming Basics
.net framework
Window to Viewport Transformation in Computer Graphics with.pptx
Android styles and themes
ASE Consulting Vee Model
How to chtmultiregionfoam
Android PPT Presentation 2018
flexray technology in modern cars
HTML5 Apps on AGL Platform with the Web Application Manager (Automotive Grade...
Android Layout
Projection In Computer Graphics
Kernel Recipes 2015: Representing device-tree peripherals in ACPI
Solutions manual for guide to sql 9th edition by pratt
Board support package_on_linux
UIStackView – Tom Bowden – Dec 2015 – Eventacular Inc
Introduction to android
iOS Architecture
Introduction to VB.net
Ad

More from Kros Huang (7)

PDF
Kotlin Receiver Types 介紹
PDF
Kotlin Data Model
PDF
Epoxy 介紹
PDF
Fastlane on Android 介紹
PDF
RxJava 2.0 介紹
PDF
Rxjava 介紹與 Android 中的 RxJava
PDF
Android with dagger_2
Kotlin Receiver Types 介紹
Kotlin Data Model
Epoxy 介紹
Fastlane on Android 介紹
RxJava 2.0 介紹
Rxjava 介紹與 Android 中的 RxJava
Android with dagger_2
Ad

Recently uploaded (20)

PDF
Model Code of Practice - Construction Work - 21102022 .pdf
PDF
SM_6th-Sem__Cse_Internet-of-Things.pdf IOT
PPTX
Lecture Notes Electrical Wiring System Components
PPTX
CH1 Production IntroductoryConcepts.pptx
PPT
Project quality management in manufacturing
PPTX
OOP with Java - Java Introduction (Basics)
PDF
Mohammad Mahdi Farshadian CV - Prospective PhD Student 2026
PPTX
FINAL REVIEW FOR COPD DIANOSIS FOR PULMONARY DISEASE.pptx
PPTX
bas. eng. economics group 4 presentation 1.pptx
PPTX
Foundation to blockchain - A guide to Blockchain Tech
PPTX
additive manufacturing of ss316l using mig welding
PDF
Well-logging-methods_new................
PPTX
Geodesy 1.pptx...............................................
DOCX
ASol_English-Language-Literature-Set-1-27-02-2023-converted.docx
PPTX
UNIT 4 Total Quality Management .pptx
PPTX
Sustainable Sites - Green Building Construction
PPT
Mechanical Engineering MATERIALS Selection
PDF
Enhancing Cyber Defense Against Zero-Day Attacks using Ensemble Neural Networks
PDF
composite construction of structures.pdf
PPTX
KTU 2019 -S7-MCN 401 MODULE 2-VINAY.pptx
Model Code of Practice - Construction Work - 21102022 .pdf
SM_6th-Sem__Cse_Internet-of-Things.pdf IOT
Lecture Notes Electrical Wiring System Components
CH1 Production IntroductoryConcepts.pptx
Project quality management in manufacturing
OOP with Java - Java Introduction (Basics)
Mohammad Mahdi Farshadian CV - Prospective PhD Student 2026
FINAL REVIEW FOR COPD DIANOSIS FOR PULMONARY DISEASE.pptx
bas. eng. economics group 4 presentation 1.pptx
Foundation to blockchain - A guide to Blockchain Tech
additive manufacturing of ss316l using mig welding
Well-logging-methods_new................
Geodesy 1.pptx...............................................
ASol_English-Language-Literature-Set-1-27-02-2023-converted.docx
UNIT 4 Total Quality Management .pptx
Sustainable Sites - Green Building Construction
Mechanical Engineering MATERIALS Selection
Enhancing Cyber Defense Against Zero-Day Attacks using Ensemble Neural Networks
composite construction of structures.pdf
KTU 2019 -S7-MCN 401 MODULE 2-VINAY.pptx

Android MvRx Framework 介紹

  • 2. Outline • 什什麼是 MvRx (唸作 mavericks) • Core Concepts • 實作細節 • Testing • 其他
  • 3. 什什麼是 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 的框架,可以⼤大幅減少撰寫的程式碼
  • 4. 幾個開發上的問題 • 複雜的 Android Lifecycle 問題 • 在 onSaveInstanceState 的處理理邏輯,與 view state 狀狀態儲存問題 • 在網路路或資料庫的非同步 (asynchronous requests) 呼叫時, onSuccess, onFailure 的處理理,thread 切換等問題 • Android 程式中預設都是 main thread,能否更更簡單的做 Threading 切 換,把複雜的計算放到 background thread
  • 5. 什什麼是 MvRx • 為了了解決這些問題,Airbnb ⾃自⾏行行開發了了⼀一套框架 • ⽬目的是讓使⽤用者可以按照固定的架構,寫出⾼高品質、好維護、效能佳、 Bug 少的 Android App • 已經⼤大幅在 Airbnb 的產品上採⽤用
  • 6. 什什麼是 MvRx • MvRx is Kotlin first and Kotlin only. • MvRx is Kotlin first and Kotlin only. • MvRx is Kotlin first and Kotlin only. • (很重要所以說三次)
  • 7. 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.
  • 8. 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
  • 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)
  • 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) 解決 Android Lifecycle 的問題
  • 11. 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,例例如網路路存取資料,資料庫 操作
  • 12. 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 概念念相似
  • 13. 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 整合
  • 14. Core Concepts • State • ViewModel • View • Async
  • 15. Core Concepts • State • ViewModel • View • Async 先看 State, ViewModel, View 之間的關係
  • 16. State • MvRxState ⽤用來來儲存每個畫⾯面所需要的資料,為不可修改的物件 (immutable object),單純儲存資料,沒有邏輯 • 本⾝身為 Data class
  • 17. ViewModel • MvRxViewModel 為 Google ViewModel 的延伸
  • 18. ViewModel • MvRxViewModel 為 Google ViewModel 的延伸 • MvRxViewModels ⽤用來來處理理所有的邏輯運算,⼀一個 ViewModel 會搭配 ⼀一個 State,可以在 ViewModel 中讀取、更更新、觀察 State
  • 19. ViewModel • MvRxViewModel 為 Google ViewModel 的延伸 • MvRxViewModels ⽤用來來處理理所有的邏輯運算,⼀一個 ViewModel 會搭配 ⼀一個 State,可以在 ViewModel 中讀取、更更新、觀察 State • 當 Configuration changes 時,Fragment、View、Activity 都會重新 產⽣生,⽽而 ViewModel 則不會 (Google ViewModel 的⾏行行為)
  • 21. ViewModel • 與 Google ViewModel 不同的是,MvRxViewModel 內操作的資料為 MvRxState (⽽而不是 LiveData),⽽而 View 只能呼叫 ViewModel 做事情並 觀察它的變化 (observable pattern)
  • 22. View • MvRxView 是 Interface,為 Android 的 LifecycleOwner 的延伸 • 使⽤用者必須在 View 實作 MvRxView (例例如 fragment), MvRxView 會 根據每次 State 的變化,呼叫 invalidate() function,通知 UI 更更新介⾯面
  • 27. MvRxView Fragment • Fragment 實作 MvRxView Interface MvRxViewModel MvRxState
  • 28. MvRxView State 有任何改變,通知 View Fragment • Fragment 呼叫 ViewModel 做事情 • State 有任何改變就會呼叫 MvRxView 的 invalidate() MvRxState MvRxViewModel
  • 29. State 有任何改變,通知 View Fragment • Fragments 之間也可以共⽤用同⼀一個 ViewModel (不同⾴頁⾯面共享資料) MvRxState MvRxViewModel MvRxView
  • 30. Core Concepts • State • ViewModel • View • Async 專⾨門為處理理 async request 所建立的 class
  • 31. Async • Async 為 Kotlin 中的 sealed class,有四個 subclass • Uninitialized • Loading • Success • Fail (其中包含⼀一個 error 欄欄位) • 在 MvRx 中,所有的 Async Request 都會⽤用 Async 表⽰示
  • 32. 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)
  • 33. Observable 整合 • ViewModel 會在 Lifecycle 結束時⾃自動捨棄 (dispose) subscription, 因此不⽤用擔⼼心 memory leak 的問題(不⽤用⼿手動 unsubscribe 啦!) • 當螢幕旋轉或是有任何 configuration changes,ViewModel 都會保留留 原來來的狀狀態,不⽤用擔⼼心 requesting 消失或狀狀態不⼀一致
  • 34. ⼩小結 • MvRx 定義出⼀一個標準的框架 • MvRx 幫你處理理 Android Lifecycle 的問題 • MvRx 幫你處理理 Configuration Changes 的問題 • MvRx 幫你解決 Fragments 共⽤用資料的問題 • MvRx 幫你處理理 Async Requests 的問題 • MvRx 讓你可以⽤用類似 React 的⽅方式開發 App
  • 38. 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 } } }
  • 39. 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
  • 40. 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
  • 41. 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
  • 42. 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
  • 43. 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
  • 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 } } } 實作 MvRxView 的 Interface
  • 45. ViewModel 細節 • ViewModel 建立⽅方式 • 存取 State • 更更新 State
  • 46. ViewModel 細節 • ViewModel 建立⽅方式 • 存取 State • 更更新 State
  • 47. 建立 ViewModel • 設定 ViewModel 建立⽅方式 • 透過 Kotlin Delegates ⽅方式建立
  • 48. ViewModel 的建立⽅方式 • 有兩兩種建立⽅方式 • 透過 ViewModel 的 Constructor class MyViewModel(initialState: MyState) : MvRxViewModel<MyState>(initialState)
  • 49. • 有兩兩種建立⽅方式 • 透過 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 的建立⽅方式
  • 50. 建立 ViewModel • MvRx 提供的 extension method 建立 ViewModel
  • 51. 建立 ViewModel • fragmentViewModel:建立或讀取現有的 ViewModel,有效範圍為 Fragment (scoped to this Fragment) • activityViewModel:建立或讀取現有的 ViewModel,有效範圍為 Activity (scoped to this Activity),通常使⽤用在多個 fragments 共享資 料 • existingViewModel:讀取現有的 ViewModel (Activity scope),若若沒 有已存在 ViewModel 則會回傳錯誤
  • 52. 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 } } }
  • 53. 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
  • 54. 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
  • 55. ViewModel 細節 • ViewModel 建立⽅方式 • 存取 State • 更更新 State
  • 56. ViewModel 讀取 State • 透過 「withState block」 • 語法原理理是什什麼? withState { state -> }
  • 57. ViewModel 讀取 State • 透過 「withState block」 • 語法原理理是什什麼? • Function literals with receiver withState { state -> }
  • 58. ViewModel 更更新 State • 透過 「setState block」 setState { copy(title = title) }
  • 59. ViewModel 更更新 State • 透過 「setState block」 setState { copy(title = title) } • 由於 State 為 immutable object,要更更新 state,就必須重建⼀一個新的 object:利利⽤用 data class 的 copy method
  • 60. ViewModel Threading • 在 ViewModel 中存取 state 的 block 是皆為 background thread • 「withState block」會等待全部的 pending「setState block」都執⾏行行 完成後,才會執⾏行行,確保「withState block」拿到的是最新的 state
  • 61. 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") } } }
  • 62. 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
  • 63. 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: >> ??
  • 64. 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"
  • 65. ViewModel Threading • MvRx 其中⼀一個核⼼心概念念就是:thread-safe • 所有 non-view 的程式都可以在 background thread 執⾏行行 • ⼤大⼤大降低使⽤用者⾃自⾏行行管理理 thread 的負擔
  • 66. View 存取 State • 透過 「withState block」 withState(viewModel) { state -> } • 表⽰示要存取特定 viewModel 中的 State
  • 67. View 存取 State • View 中的 「withState block」為 main thread 呼叫,呼叫時會直接回 傳⽬目前的 state 的 snapshot • 直接當作 function ⽤用即可
  • 68. 存取 State • withState block • setState block • 運⾏行行在 background thread • withState block • 運⾏行行在 main thread ViewModel View
  • 70. 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 } } }
  • 71. 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
  • 72. 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
  • 73. 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 的資料
  • 74. 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()
  • 75. 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 並顯⽰示在畫⾯面上
  • 78. 範例例⼆二 - Sign In • 沒有輸入密碼,Client 端顯⽰示錯誤 • 輸入錯誤的密碼,顯⽰示 Server 的錯誤訊息 • 登入成功,關掉此⾴頁⾯面 • 登入時顯⽰示 Loading
  • 80. 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
  • 81. 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
  • 82. 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
  • 83. 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
  • 84. 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>
  • 85. 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,須做兩兩件事:
  • 86. 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
  • 87. 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
  • 88. 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 } } } }
  • 89. 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 執⾏行行登入
  • 90. 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
  • 91. • 除了了 invalidate() 之外,有其他⽅方法可以觀察 state 嗎? Subscribing to state manually
  • 92. Subscribing to state manually • MvRx 提供三種額外⼿手動⽅方式觀察 state • subscribe - 觀察整個 state • selectSubscribe - 只觀察 state 中的特定 property • asyncSubscribe - 只觀察 Async 類別的 property
  • 93. 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
  • 94. 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
  • 95. 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
  • 97. Test ViewModel • ViewMode 是可測試的 • ⽬目前 MvRx (v0.5.0) 測試環境設定稍嫌複雜,issue 中有提到之後會改進
 (請參參考 MvRx project 中的 BaseTest class 設定) • 需搭配 Robolectric
  • 98. Test ViewModel • 範例例:測試輸入空⽩白密碼 • Test case:當使⽤用者輸入空⽩白密碼,系統會回傳錯誤,並攜帶錯誤訊息
 「empty email or password」
  • 99. 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) } } }
  • 100. 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("test@mail.com", "") } }
  • 101. 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("test@mail.com", "") } } 定義測試專⽤用的 LifecycleOwner
  • 102. 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("test@mail.com", "") } }
  • 103. 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("test@mail.com", "") } }
  • 104. ⼩小結 • MvRx 定義出⼀一個標準的框架 • MvRx 幫你處理理 Android Lifecycle 的問題 • MvRx 幫你處理理 configuration changes 的問題 • MvRx 幫你解決 Fragments 共⽤用資料的問題 • MvRx 幫你處理理 Async Requests 的問題 • MvRx 讓你可以⽤用類似 React 的⽅方式開發 App • MvRx 為 Thread-safe • MvRx 是可測試的
  • 105. 其他
  • 106. Dependency Injection • 可以跟什什麼 DI library 整合? • Koin • Dagger 2 (可搭配 AssistedInject)
  • 107. Kotlin coroutines • ⽬目前不⽀支援 coroutines,只⽀支援 RxJava • 若若有興趣,可⾃自⾏行行把 coroutines 包裝成 Async • 有討論中的 issue:
 https://github.com/airbnb/MvRx/issues/79 • 指⽇日可待?
  • 108. Epoxy • Epoxy 為 Airbnb 另⼀一個開源 library,⽤用來來建立複雜的 RecyclerView Layout • 可與 MvRx 整合,當 state 更更新時,只會更更新有變化的 element • 寫起來來更更 Reactive
  • 109. 優點 • 全原⽣生開發,與你的 code 100% 相容 • 導入快速 (可只導入單⼀一⾴頁⾯面) • 節省開發時間 • 按照框架可以簡單寫出⾼高品質的程式碼
  • 110. 缺點 • 學習⾨門檻稍⾼高 • 以 Airbnb 的訴求為主,提需求不⼀一定會被接受
 (例例如現在只接受 Fragment ) • Unit Test 設定複雜,待⽇日後改進 • 正在開發中,API 還會變動
  • 111. 要怎麼開始導入? • Convert Java to Kotlin • 修改 Base Activity • 修改 Base Fragment • 建立 Base ViewModel
  • 114. 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
  • 115. 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