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

MVI : une architecture robuste et moderne pour vos applications mobiles

MVI : une architecture robuste et moderne pour vos applications mobiles

Co-présentée par Arnaud Piroelle et Simone Civetta

Oui, on le sait : vous en avez assez des talks d'architecture mobile. On vous comprend, vous avez dû en voir des tonnes : utiles, superflus, simples, alambiqués, certains même imprononçables et là vous n’en pouvez plus.

Mais donnez-nous une chance : cette fois-ci nous voulons vous présenter une architecture que nous apprécions véritablement : simple, robuste, facilement testable et tirant profit des langages modernes comme Kotlin et Swift - Model-View-Intention. Inspirée de Redux, mais adaptée aux applications mobiles, MVI se sert d'immutabilité, flux de données unidirectionnel et binding pour faciliter débogage et testing et améliorer donc votre productivité. Applicable à Android et iOS, elle permet aussi de simplifier les échanges entre les équipes de développeurs d'applications mobiles natives.

Nous vous présenterons les éléments clés de l'architecture, sa mise en place, ainsi que l'implémentation des cas d'usage les plus communs, issus d'applications que nous avons réellement développées.

Simone Civetta

April 24, 2019
Tweet

More Decks by Simone Civetta

Other Decks in Programming

Transcript

  1. 3

  2. Moi, mon archi, c’est du [INSÉRER UNE QUANTITÉ LIMITÉE DE

    LETTRES AU HASARD] Un dév au hasard 13
  3. #DEFINE ARCHITECTURE On peut identifier plusieurs scénarios d’architecture : -

    Architecture de Système - Architecture de Navigation - Architecture de Présentation 16
  4. #DEFINE ARCHITECTURE On peut identifier plusieurs scénarios d’architecture : -

    Architecture de Système (Onion, Clean, ...) - Architecture de Navigation (Router, Coordinators) - Architecture de Présentation (MVC, MVP, MVVM, …) 17
  5. Répartition du code dans une appli moyenne de code concerne

    la présentation (dans une application moyenne) Source : StackOverflow 19
  6. ~50% de code concerne la couche de présentation (dans une

    application moyenne) 20 Source : StackOverflow
  7. Le caractéristiques du code de la couche de présentation -

    Généralement Stateful - Difficile à tester - Sujet à changement - Modifié par un grand nombre d’intervenants - Devient de plus en plus complexe avec le temps - Usine à bugTM 21
  8. 23

  9. MVC • 1/2 class MyActivity: Activity() { val taskService =

    TaskService() lateinit var tasksTextView: TextView lateinit var loadingIndicator: ProgressBar lateinit var errorView: View lateinit var emptyView: View override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... loadTasks() } // Continues... 24
  10. MVC • 2/2 fun loadTasks() { loadingIndicator.visibility = VISIBLE taskService.loadTasks

    { tasks, error -> loadingIndicator.visibility = GONE errorView.visibility = if (error == null) GONE else VISIBLE if (error != null) return emptyView.visibility = if (tasks.isEmpty()) VISIBLE else GONE tasksTextView.text = tasks.allTitles() } } } 25
  11. iOS : MVC • Recap class MyViewController: UIViewController { let

    taskService = TaskService() @IBOutlet weak var tasksTextView: UITextView! @IBOutlet weak var loadingIndicator: UIActivityIndicatorView! @IBOutlet weak var errorView: UIView! @IBOutlet weak var emptyView: UIView! override func viewDidLoad() { super.viewDidLoad() loadTasks() } func loadTasks() { loadingIndicator.startAnimating() taskService.loadTasks { [weak self] tasks, error in loadingIndicator.stopAnimating() self?.errorView.isHidden = error != nil guard error != nil else { return } self?.emptyView.isHidden = !(tasks?.isEmpty ?? true) self?.tasksTextView.text = tasks?.allTitles() } } } 26
  12. 27

  13. MVP 1/3 public interface MyContract { interface View { fun

    setLoadingIndicator(active: Boolean) fun showTasks(tasks: List<Task>) fun showLoadingTasksError() fun showNoTasks() } interface Presenter { fun loadTasks(forceUpdate: Boolean) } } 28
  14. MVP 2/3 class MyFragment : Fragment(), MyContract.View { private val

    presenter: MyContract.Presenter by lazy { MyPresenter(this) } fun onResume(){ super.onResume() presenter.loadTasks(true) } override fun setLoadingIndicator(active: Boolean) { // Do something } override fun showTasks(tasks: List<Task>) { // Do something } override fun showLoadingTasksError() { // Do something } override fun showNoTasks() { // Do something } } 29
  15. Android : MVP 3/3 class MyPresenter(val view: MyContract.View) : MyContract.Presenter

    { override fun loadTasks(forceUpdate: Boolean) { view.setLoadingIndicator(true) try { val tasks = repository.loadTasks() if (tasks.isEmpty()) { view.showNoTasks() } else { view.showTasks(tasks) } view.setLoadingIndicator(false) } catch(e: LoadingException){ view.showLoadingTasksError() view.setLoadingIndicator(false) } } } 30
  16. 32

  17. Simple, mais... MVC - Couplage fort entre métier et présentation

    - La logique métier se perd dans la logique de vue - Le fonctionnement dépend du cycle de vie de l’Activity/Fragment - Approche impérative - Les comportements sont difficiles à mocker - Le système est difficile à tester 34
  18. Simple, mais... MVP - Couplage fort entre la vue et

    son presenter - La vue définit implicitement l’état... - ...et est responsable des actions envoyées au Presenter - Approche impérative 35
  19. All problems in computer science can be solved by another

    level of indirection - Moi, quand je dois dire “non” au client et que j’envoie mon collègue à ma place 37
  20. MVVM : ViewModel class MyViewModel : ViewModel(){ val taskService =

    TaskService() val taskText = MutableLiveData<String>() val isLoading = MutableLiveData<Boolean>().apply { value = false } val errorViewHidden = MutableLiveData<Boolean>().apply { value = true } val emptyViewHidden = MutableLiveData<Boolean>().apply { value = true } fun loadTasks() { isLoading.value = true taskService.loadTasks { tasks, error -> isLoading.value = false errorViewHidden.value = error == nil emptyViewHidden.value = !(tasks?.isEmpty() :? true) taskText.value = tasks?.allTitles() } } } 38
  21. MVVM : ViewModel class MyViewModel : ViewModel(){ val taskService =

    TaskService() val taskText = MutableLiveData<String>() val isLoading = MutableLiveData<Boolean>().apply { value = false } val errorViewHidden = MutableLiveData<Boolean>().apply { value = true } val emptyViewHidden = MutableLiveData<Boolean>().apply { value = true } fun loadTasks() { isLoading.value = true taskService.loadTasks { tasks, error -> isLoading.value = false errorViewHidden.value = error == nil emptyViewHidden.value = !(tasks?.isEmpty() :? true) taskText.value = tasks?.allTitles() } } } 39
  22. MVVM : ViewModel class MyViewModel : ViewModel(){ val taskService =

    TaskService() val taskText = MutableLiveData<String>() val isLoading = MutableLiveData<Boolean>() val errorViewHidden = MutableLiveData<Boolean>() val emptyViewHidden = MutableLiveData<Boolean>() fun loadTasks() { isLoading.value = true taskService.loadTasks { tasks, error -> isLoading.value = false errorViewHidden.value = error == nil emptyViewHidden.value = !(tasks?.isEmpty() :? true) taskText.value = tasks?.allTitles() } } } LiveData mutable ! 40
  23. MVVM : View class MyActivity: Activity() { val viewModel: MyViewModel

    by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... viewModel.taskText.observe(this, Observer { taskTextView.text = it }) viewModel.errorViewHidden.observe(this, Observer { errorView.setVisible(it) }) viewModel.emptyViewHidden.observe(this, Observer { emptyView.setVisible(it) }) viewModel.isLoading.observe(this, Observer { editTextView.setVisible(it) }) viewModel.loadTasks() } } 41
  24. MVVM : View class MyActivity: Activity() { val viewModel: MyViewModel

    by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... viewModel.taskText.observe(this, Observer { taskTextView.text = it }) viewModel.errorViewHidden.observe(this, Observer { errorView.setVisible(it) }) viewModel.emptyViewHidden.observe(this, Observer { emptyView.setVisible(it) }) viewModel.isLoading.observe(this, Observer { editTextView.setVisible(it) }) viewModel.loadTasks() } } 42
  25. Testabilité de MVVM // Prepare val mockService: TaskService = MockTaskService(listOf("Bananas"))

    val viewModel = MyViewModel(mockService) // Run viewModel.loadTasks() // Verify assertEquals(true, viewModel.errorViewHidden.value) assertEquals(true, viewModel.emptyViewHidden.value) assertEquals("Bananas", viewModel.taskText.value) 43
  26. Un exemple plus complexe • 1 class MyViewModel : ViewModel()

    { val taskText = MutableLiveData<String>() fun loadTasks() { taskService.loadTasks { tasks, error -> taskText.value = tasks?.allTitles() } } } 46
  27. Un exemple plus complexe • 2 class MyViewModel : ViewModel()

    { // L’Activity peut modifier la valeur du LiveData, ce qui comportera // implicitement des side-effects car le ViewModel est muable // Nous devons donc implémenter une sorte de contrôle d’accès val taskText = MutableLiveData<String>() fun loadTasks() { taskService.loadTasks { tasks, error -> taskText.value = tasks?.allTitles() } } } 47
  28. Un exemple plus complexe • 2 - Solution class MyViewModel

    : ViewModel() { private val _taskText = MutableLiveData<String>() val taskText: LiveData<String> get() = _taskText fun loadTasks() { taskService.loadTasks { tasks, error -> _taskText.value = tasks?.allTitles() } } } 48
  29. Un exemple plus complexe • 3 class MyViewModel : ViewModel()

    { private val _taskText = MutableLiveData<String>() private val _appointmentText = MutableLiveData<String>() val taskText: LiveData<String> get() = _taskText val appointmentText: LiveData<String> get() = _appointmentText func loadTasks() { /* [...] */ } func loadAppointments() { /* [...] */ } 49
  30. Un exemple plus complexe • 3 class MyViewModel : ViewModel()

    { private val _taskText = MutableLiveData<String>() private val _appointmentText = MutableLiveData<String>() val taskText: LiveData<String> get() = _taskText val appointmentText: LiveData<String> get() = _appointmentText // ⇐ true where NO taskError and NO appointmentError val errorViewHidden: LiveData<Boolean> } 50
  31. Un exemple plus complexe • 3 - Solution class MyViewModel

    : ViewModel() { private val _taskText = MutableLiveData<String>() private val _appointmentText = MutableLiveData<String>() val taskText: LiveData<String> get() = _taskText val appointmentText: LiveData<String> get() = _appointmentText private val _taskError = MutableLiveData<Error>() private val _appointmentError = MutableLiveData<Error>() val errorViewHidden = MediatorLiveData<Boolean>() init() { errorViewHidden.addSource(_taskError) {...} errorViewHidden.addSource(_appointmentError) {...} } } 51
  32. MVVM : Attente VS Réalité class MyViewModelExpectation : ViewModel() {

    val taskService = TaskService() val appointmentService = AppointmentService() val taskText = MutableLiveData<String>() val taskError = MutableLiveData<Error>() val appointmentText = MutableLiveData<String>() val appointmentError = MutableLiveData<Error>() val errorViewHidden = MediatorLiveData<Boolean>() init() { errorViewHidden.addSource(taskError) {...} errorViewHidden.addSource(appointmentError) {...} } fun loadTasks() { taskService.loadTasks { tasks, error -> taskError.value = error taskText.value = tasks?.allTitles() } } fun loadAppointments() { appointmentService.loadAppointments { appointments, error -> appointmentError.value = error appointmentText.value = appointments?.allTitles() } } class MyViewModelReality : ViewModel() { val taskService = TaskService() val appointmentService = AppointmentService() private val _taskText = MutableLiveData<String>() private val _appointmentText = MutableLiveData<String>() private val _taskError = MutableLiveData<Error>() private val _appointmentError = MutableLiveData<Error>() val taskText: LiveData<String> get() = _taskText val appointmentText: LiveData<String> get() = _appointmentText val errorViewHidden: LiveData<Boolean> init() { errorViewHidden.addSource(_taskError) {...} errorViewHidden.addSource(_appointmentError) {...} } func loadTasks() { taskService.loadTasks { tasks, error -> _taskError.value = error _taskText.value = tasks?.allTitles() } } func loadAppointments() { appointmentService.loadAppointments { appointments, error -> _appointmentError.value = error _appointmentText.value = appointments?.allTitles() } } } 52
  33. Et si on voulait tester ? // Prepare val mockTaskService

    = MockTaskService(listOf(Task(name= "Prepare the talk"))) val mockAppointmentService = MockAppointmentService(listOf(Appointment(name= "Talk @ Android Makers"))) val viewModel = MyViewModel(mockTaskService, mockAppointmentService) // Run viewModel.loadTasks() viewModel.loadAppointments() // Verify // ⇐ true where NO taskError and NO appointmentError assertEquals(true, viewModel.errorViewHidden.value) 53
  34. Et si on voulait tester ? // Prepare val mockTaskService

    = MockTaskService(listOf(Task(name= "Prepare the talk"))) val mockAppointmentService = MockAppointmentService(listOf(Appointment(name= "Talk @ AndroidMakers"))) val viewModel = MyViewModel(mockTaskService, mockAppointmentService) // Run viewModel.loadTasks() viewModel.loadAppointments() // Verify // ⇐ true where NO taskError and NO appointmentError assertEquals(true, viewModel.errorViewHidden.value) 54
  35. Et si on voulait tester ? // Prepare val mockTaskService

    = MockTaskService(listOf(Task(name: "Prepare the talk"))) val mockAppointmentService = MockAppointmentService(listOf(Appointment(name: "AndroidMakers"))) val viewModel = MyViewModel(mockTaskService, mockAppointmentService) // Run viewModel.loadTasks() viewModel.loadAppointments() // Verify // ⇐ true where NO taskError and NO appointmentError assertEquals(true, viewModel.errorViewHidden.value) // Mais Comment tester les side-effects d'un des services en isolation ? (c-a-d, sans exécuter loadTasks ET loadAppointments?) 55
  36. MVVM Benefices : - Séparation de la logique métier de

    la logique de vue - Bindings : la vue devient extrêmement simple - Testabilité accrue 56
  37. MVVM Complexités : - Contrôle d’accès verbeux - Inspectabilité de

    l’état courant - Isolation des side-effects de chaque action lors des tests - Nécessite des compétences Reactive 57
  38. Une première découpe : un State immuable data class MyState(

    val taskText: String? = null val appointmentText: String? = null val taskError: Error? = null val appointmentError: Error? = null ) 60
  39. Une première découpe : un State immuable data class MyState

    { val taskText: String? = null val appointmentText: String? = null val taskError: Error? = null val appointmentError: Error? = null ) { val errorViewHidden: Boolean get() = (taskError == null && appointmentError == null) } 61
  40. ...et son ViewModel • 1 class MyViewModel: ViewModel() { val

    taskService = TaskService() val appointmentService = AppointmentService() private val _state = MutableLiveData<MyState>().apply { value = MyState() } val state: LiveData<MyState> get() = _state 62
  41. ...et son ViewModel • 2 fun loadTasks() { taskService.loadTasks {

    tasks, error -> val currentState = _state.value ?: MyState() val newState = currentState.copy( taskText = tasks?.allTitles(), taskError = error ) _state.value = newState } } fun loadAppointments() { appointmentService.loadAppointments { [weak self] (appointments, 63
  42. ...et son ViewModel • 2 _state.value = newState } }

    fun loadAppointments() { appointmentService.load { appointments, error -> val currentState = _state.value ?: MyState() val newState = currentState.copy( appointmentText = appointments?.allTitles(), appointmentError = error ) _state.value = newState } } } 64
  43. Enlevons le code redondant... fun loadTasks() { taskService.loadTasks { tasks,

    error -> val currentState = _state.value ?: MyState() val newState = currentState.copy( taskText = tasks?.allTitles(), taskError = error ) _state.value = newState } } fun loadAppointments() { appointmentService.loadAppointments { appointments, error -> val currentState = _state.value ?: MyState() val newState = currentState.copy( appointmentText = appointments?.allTitles(), appointmentError = error ) _state.value = newState } } 65
  44. Enlevons le code redondant... fun loadTasks() { taskService.loadTasks { tasks,

    error -> val currentState = _state.value ?: MyState() val newState = currentState.copy( taskText = tasks?.allTitles(), taskError = error ) _state.value = newState } } fun loadAppointments() { appointmentService.loadAppointments { appointments, error -> val currentState = _state.value ?: MyState() val newState = currentState.copy( appointmentText = appointments?.allTitles(), appointmentError = error ) _state.value = newState } } 66
  45. Enlevons le code redondant... open class BaseViewModel<S: State>(private val initialState:

    S) { private val _state = MutableLiveData<S>().apply { value = initialState } val state: LiveData<S> get() = _state fun updateState(handler: (S) -> (S)) { val currentState = _state.value ?: initialState _state.value = handler(currentState) } } 67
  46. class MyViewModel(intialState: MyState): BaseViewModel<MyState>(intialState) { val taskService = TaskService() val

    appointmentService = AppointmentService() fun loadTasks() { taskService.loadTasks { tasks, error -> updateState { state -> state.copy(taskText = tasks?.allTitles(), taskError = error) } } } fun loadAppointments() { appointmentService.loadAppointments { appointments, error -> updateState { state -> state.copy( appointmentText = appointments?.allTitles(), appointmentError = error ) } } } } 68
  47. class MyViewModel(intialState: MyState): BaseViewModel<MyState>(intialState) { val taskService = TaskService() val

    appointmentService = AppointmentService() fun loadTasks() { taskService.loadTasks { tasks, error -> updateState { state -> state.copy(taskText = tasks?.allTitles(), taskError = error) } } } fun loadAppointments() { appointmentService.loadAppointments { appointments, error -> updateState { state -> state.copy( appointmentText = appointments?.allTitles(), appointmentError = error ) } } } } class MyViewModelReality : ViewModel() { val taskService = TaskService() val appointmentService = AppointmentService() private val _taskText = MutableLiveData<String>() private val _appointmentText = MutableLiveData<String>() private val _taskError = MutableLiveData<Error>() private val _appointmentError = MutableLiveData<Error>() val taskText: LiveData<String> get() = _taskText val appointmentText: LiveData<String> get() = _appointmentText val errorViewHidden: LiveData<Boolean> init() { errorViewHidden.addSource(_taskError) {...} errorViewHidden.addSource(_appointmentError) {...} } fun loadTasks() { taskService.loadTasks { tasks, error -> _taskError.value = error _taskText.value = tasks?.allTitles() } } fun loadAppointments() { appointmentService.loadAppointments { appointments, error -> _appointmentError.value = error _appointmentText.value = appointments?.allTitles() } } } 69
  48. Et la view ? class MyActivity: Activity { var viewModel:

    MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) [ ... ] viewModel.state.map { it.taskText }.distinctUntilChanged() .bind(this, this::onTaskTextChanged) viewModel.state.map { it.appointmentText }.distinctUntilChanged() .bind(this, this::onAppointmentTextChanged) viewModel.loadTasks() } } 70
  49. Et la view ? class MyActivity: Activity { var viewModel:

    MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) … viewModel.state.map { it.taskText }.distinctUntilChanged() .bind(this, this::onTaskTextChanged) viewModel.state.map { it.appointmentText }.distinctUntilChanged() .bind(this, this::onAppointmentTextChanged) viewModel.loadTasks() } } 71
  50. Et les tests ? // Prepare val mockTaskService = MockTaskService(listOf(Task(name:

    "Prepare the talk"))) val mockAppointmentService = MockAppointmentService(listOf( Appointment(name: "Talk @ Android Makers"))) val viewModel = MyViewModel(mockTaskService, mockAppointmentService, MyState()) // Run viewModel.loadTasks() viewModel.loadAppointments() // Et si on voulait tester en isolation ? // Verify assertEquals(true, viewModel.errorViewHidden.value) 72
  51. Et les tests ? // Prepare val mockTaskService = MockTaskService(listOf(Task(name:

    "Prepare the talk"))) val mockAppointmentService = MockAppointmentService(listOf( Appointment(name: "Talk @ Android Makers"))) val state = MyState(taskError = MyError()) val viewModel = MyViewModel(mockTaskService, mockAppointmentService, state) // Run viewModel.loadAppointments() // Verify assertEquals(true, viewModel.state.errorViewHidden.value) 73
  52. Qu’est-ce qu’on vient de voir ? - Un state immuable,

    qui réduit les side effects - Une représentation complète et facilement compréhensible du state - Des tests simplifiés et plus précis 74
  53. Où est l’intention ? sealed class MyAction { object LoadTasks:

    MyAction() object LoadAppointments: MyAction() } 76
  54. Où est l’intention ? abstract class BaseViewModel<S: State, A: Action>

    { [...] abstract fun handle(action: A) [...] } interface Action interface State 77
  55. Où est l’intention ? class MyViewModel: BaseViewModel<MyState, MyAction> { override

    fun handle(action: MyAction) { when (action) { is LoadTasks -> loadTasks() is LoadAppointments -> loadAppointments() } } private fun loadTasks() { taskService.loadTasks { tasks, error -> updateState { state -> state.copy( taskText = tasks?.allTitles(), taskError = error ) } } } 78
  56. Et la view ? class MyActivity: Activity { var viewModel:

    MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) [...] //state bindings tasksButton.setOnClickListener { viewModel.handle(MyAction.LoadTasks) } appointmentsButton.setOnClickListener { viewModel.handle(MyAction.LoadAppointments) } } } 79
  57. Et la view ? class MyActivity: Activity { var viewModel:

    MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) [...] tasksButton.setOnClickListener { viewModel.handle(MyAction.LoadTasks) } appointmentsButton.setOnClickListener { viewModel.handle(MyAction.LoadAppointments) } } } 80
  58. Les bénéfices de l’Action - Interdit les bindings bidirectionnels -

    Décrit l’API de l’écran - Définit un formalisme pour la communication entre View et ViewModel - En Kotlin, les when doivent être exhaustifs 81
  59. Les concepts clés - ViewModel - State (Immuable) - Intentions

    (ou Actions) - Unidirectional Data Flow - Absence de two-way-bindings - Echange de donnée plus contraignant, mais simplifié - Plus facile à tester 84
  60. Quelques déjà vu peut-être ? Mêmes concepts que sur :

    - Elm architecture - Redux - MvRx (AirBnb) 85
  61. Collaboration Android et iOS : Action sealed class Action {

    object LoadTasksAction: Action() object LoadAppointmentsAction: Action() } 87
  62. Collaboration Android et iOS : Action enum Action { case

    loadTasks case loadAppointments } 88
  63. Collaboration Android et iOS : State data class MyState( val

    isLoading: Boolean = false, val taskText: String? = null, val taskError: Error? = null ) 89
  64. Collaboration Android et iOS : State struct MyState { var

    isLoading: Bool = false var taskText: String? = nil var taskError: Error? = nil } 90
  65. Collaboration Android et iOS : ViewModel class MyViewModel: BaseViewModel<MyState, MyAction>

    { fun handle(action: MyAction, state: MyState){ when(action){ is LoadTasksAction -> loadTasks(state) is LoadAppointmentsAction -> loadAppointments(state) } } private fun loadTasks(state: MyState) { if (state.isLoading) { return } repository.loadTasks { tasks, error -> updateState { state -> state.copy( isLoading = false, taskText = tasks.allTitles(), taskError = error )} } } } 91
  66. Collaboration Android et iOS : ViewModel class MyViewModel: BaseViewModel<MyState, MyAction>

    { func handle(action: MyAction, state: MyState) { switch action { case .loadTasks: loadTasks(state: state) case .loadAppointments: loadAppointments(state: state) } } private func loadTasks(state: MyState) { guard !state.isLoading else { return } taskService.loadTasks { [weak self] (tasks, error) in self?.updateState { $0.isLoading = false; $0.taskText = tasks?.allTitles(); $0.taskError = error } } } 92
  67. Collaboration Android et iOS : View class TasksFragment : Fragment()

    { val viewModel: TasksViewModel by viewModel() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.state.map { it.isLoading }.distinctUntilChanged() .bind(this, this::onLoadingChanged) viewModel.state.map { it.taskText }.distinctUntilChanged() .bind(this, this::onTasksChanged) } } 93
  68. Collaboration Android et iOS : View class MyViewController: BaseViewController<MyState, MyAction,

    MyViewModel> { var viewModel: MyViewModel? public override func setup(context: SettingsContext) { super.setup(context: context) stateBindings = [ viewModel.state.map { $0.isLoading }.distinctUntilChanged() .bind(loadingIndicator.rx.isAnimating) viewModel.state.map { $0.taskText }.distinctUntilChanged() .bind(tasksTextView.rx.text) ] } } 94
  69. MVI Benefices : - Séparation de la logique métier et

    de la logique de vue - Testabilité accrue (y compris isolation des side-effects) - Bindings : la vue devient extrêmement simple - Inspectabilité de l’état - Possibilité de sérialiser (et donc rejouer) actions et de ré-appliquer états 96
  70. MVI Complexités : - Un niveau d’indirection de plus -

    Prise en main plus complexe que sur MVVM - Nécessite de compétences Reactive - [Android] Pas entièrement compatible avec les composants JetPack (Navigation et Paging) - Peut nécessiter l’ajout d’une State Machine 97
  71. All problems in computer science can be solved by another

    level of indirection ...except for the problem of too many layers of indirection. - - Moi, quand je dois dire “non” au client et que j’envoie mon collègue à ma place, qui envoie son collègue à sa place, qui est bourré 98
  72. All problems in computer science can be solved by another

    level of indirection ...except for the problem of too many layers of indirection. - - Moi, quand je dois dire “non” au client et que j’envoie mon collègue à ma place, qui envoie son collègue à sa place, qui est bourré 99