Slide 1

Slide 1 text

une architecture robuste et moderne pour vos applications mobiles

Slide 2

Slide 2 text

Simone Civetta Développeur, plutôt iOS Arnaud Piroelle Développeur, plutôt Android 2

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

une architecture robuste et moderne pour vos applications mobiles

Slide 5

Slide 5 text

PHOTO_BATIMENT_TROUVÉE_SUR_GOOGLE_ SANS_PAYER_LES_ROYALTIES.JPG 5

Slide 6

Slide 6 text

- C koi ton archi ? 6

Slide 7

Slide 7 text

Moi, mon archi, c’est du MVP Un dév au hasard 7

Slide 8

Slide 8 text

Moi, mon archi, c’est du MVVM Un dév au hasard 8

Slide 9

Slide 9 text

Moi, mon archi, c’est du VIPER Un dév au hasard 9

Slide 10

Slide 10 text

Moi, mon archi, c’est de la CLEAN Un dév au hasard 10

Slide 11

Slide 11 text

Moi, mon archi, c’est de la HEXA Un dév au hasard 11

Slide 12

Slide 12 text

Moi, mon archi, c’est du MVC Un dév au hasard 12

Slide 13

Slide 13 text

Moi, mon archi, c’est du [INSÉRER UNE QUANTITÉ LIMITÉE DE LETTRES AU HASARD] Un dév au hasard 13

Slide 14

Slide 14 text

- C koi ton archi ? - Qu’est-ce que t’entends par archi ? 14

Slide 15

Slide 15 text

- Qu’est-ce que t’entends par archi ? 15

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

#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

Slide 18

Slide 18 text

Le plus grand défi d’une application mobile : la couche de présentation 18

Slide 19

Slide 19 text

Répartition du code dans une appli moyenne de code concerne la présentation (dans une application moyenne) Source : StackOverflow 19

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Réduire les risques de bugs de la couche de présentation : l’architecture 22

Slide 23

Slide 23 text

23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

27

Slide 28

Slide 28 text

MVP 1/3 public interface MyContract { interface View { fun setLoadingIndicator(active: Boolean) fun showTasks(tasks: List) fun showLoadingTasksError() fun showNoTasks() } interface Presenter { fun loadTasks(forceUpdate: Boolean) } } 28

Slide 29

Slide 29 text

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) { // Do something } override fun showLoadingTasksError() { // Do something } override fun showNoTasks() { // Do something } } 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

THIS_IS_FINE.JPG 31

Slide 32

Slide 32 text

32

Slide 33

Slide 33 text

THIS_IS_FINE.JPG 33

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Une couche d’indirection de plus 36

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

MVVM : ViewModel class MyViewModel : ViewModel(){ val taskService = TaskService() val taskText = MutableLiveData() val isLoading = MutableLiveData().apply { value = false } val errorViewHidden = MutableLiveData().apply { value = true } val emptyViewHidden = MutableLiveData().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

Slide 39

Slide 39 text

MVVM : ViewModel class MyViewModel : ViewModel(){ val taskService = TaskService() val taskText = MutableLiveData() val isLoading = MutableLiveData().apply { value = false } val errorViewHidden = MutableLiveData().apply { value = true } val emptyViewHidden = MutableLiveData().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

Slide 40

Slide 40 text

MVVM : ViewModel class MyViewModel : ViewModel(){ val taskService = TaskService() val taskText = MutableLiveData() val isLoading = MutableLiveData() val errorViewHidden = MutableLiveData() val emptyViewHidden = MutableLiveData() 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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

HAPPY_MINIONS.GIF 44

Slide 45

Slide 45 text

MONDE_DES_BISOUNOURS.GIF 45

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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() fun loadTasks() { taskService.loadTasks { tasks, error -> taskText.value = tasks?.allTitles() } } } 47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Un exemple plus complexe • 3 class MyViewModel : ViewModel() { private val _taskText = MutableLiveData() private val _appointmentText = MutableLiveData() val taskText: LiveData get() = _taskText val appointmentText: LiveData get() = _appointmentText // ⇐ true where NO taskError and NO appointmentError val errorViewHidden: LiveData } 50

Slide 51

Slide 51 text

Un exemple plus complexe • 3 - Solution class MyViewModel : ViewModel() { private val _taskText = MutableLiveData() private val _appointmentText = MutableLiveData() val taskText: LiveData get() = _taskText val appointmentText: LiveData get() = _appointmentText private val _taskError = MutableLiveData() private val _appointmentError = MutableLiveData() val errorViewHidden = MediatorLiveData() init() { errorViewHidden.addSource(_taskError) {...} errorViewHidden.addSource(_appointmentError) {...} } } 51

Slide 52

Slide 52 text

MVVM : Attente VS Réalité class MyViewModelExpectation : ViewModel() { val taskService = TaskService() val appointmentService = AppointmentService() val taskText = MutableLiveData() val taskError = MutableLiveData() val appointmentText = MutableLiveData() val appointmentError = MutableLiveData() val errorViewHidden = MediatorLiveData() 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() private val _appointmentText = MutableLiveData() private val _taskError = MutableLiveData() private val _appointmentError = MutableLiveData() val taskText: LiveData get() = _taskText val appointmentText: LiveData get() = _appointmentText val errorViewHidden: LiveData 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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

MVVM Benefices : - Séparation de la logique métier de la logique de vue - Bindings : la vue devient extrêmement simple - Testabilité accrue 56

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Au delà de MVVM : 58

Slide 59

Slide 59 text

BUZZ_LIGHTYEAR_ VERS_L_INFINI_ ET_AU_DELA_ NO_WATERMARK.JPG 59

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

...et son ViewModel • 1 class MyViewModel: ViewModel() { val taskService = TaskService() val appointmentService = AppointmentService() private val _state = MutableLiveData().apply { value = MyState() } val state: LiveData get() = _state 62

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Enlevons le code redondant... open class BaseViewModel(private val initialState: S) { private val _state = MutableLiveData().apply { value = initialState } val state: LiveData get() = _state fun updateState(handler: (S) -> (S)) { val currentState = _state.value ?: initialState _state.value = handler(currentState) } } 67

Slide 68

Slide 68 text

class MyViewModel(intialState: MyState): BaseViewModel(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

Slide 69

Slide 69 text

class MyViewModel(intialState: MyState): BaseViewModel(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() private val _appointmentText = MutableLiveData() private val _taskError = MutableLiveData() private val _appointmentError = MutableLiveData() val taskText: LiveData get() = _taskText val appointmentText: LiveData get() = _appointmentText val errorViewHidden: LiveData 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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

Model-View-Intention ? 75

Slide 76

Slide 76 text

Où est l’intention ? sealed class MyAction { object LoadTasks: MyAction() object LoadAppointments: MyAction() } 76

Slide 77

Slide 77 text

Où est l’intention ? abstract class BaseViewModel { [...] abstract fun handle(action: A) [...] } interface Action interface State 77

Slide 78

Slide 78 text

Où est l’intention ? class MyViewModel: BaseViewModel { 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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

Les concepts clés de MVI 82

Slide 83

Slide 83 text

Les concepts clés - ViewModel - State (Immuable) - Intentions (ou Actions) 83

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

Quelques déjà vu peut-être ? Mêmes concepts que sur : - Elm architecture - Redux - MvRx (AirBnb) 85

Slide 86

Slide 86 text

Bonus : collaboration iOS et Android 86

Slide 87

Slide 87 text

Collaboration Android et iOS : Action sealed class Action { object LoadTasksAction: Action() object LoadAppointmentsAction: Action() } 87

Slide 88

Slide 88 text

Collaboration Android et iOS : Action enum Action { case loadTasks case loadAppointments } 88

Slide 89

Slide 89 text

Collaboration Android et iOS : State data class MyState( val isLoading: Boolean = false, val taskText: String? = null, val taskError: Error? = null ) 89

Slide 90

Slide 90 text

Collaboration Android et iOS : State struct MyState { var isLoading: Bool = false var taskText: String? = nil var taskError: Error? = nil } 90

Slide 91

Slide 91 text

Collaboration Android et iOS : ViewModel class MyViewModel: BaseViewModel { 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

Slide 92

Slide 92 text

Collaboration Android et iOS : ViewModel class MyViewModel: BaseViewModel { 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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

Collaboration Android et iOS : View class MyViewController: BaseViewController { 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

Slide 95

Slide 95 text

Un bilan 95

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

Questions ? 100

Slide 101

Slide 101 text

Questions ? 101

Slide 102

Slide 102 text

7 et 8 octobre 2019 Beffroi de Montrouge CocoaHeads Paris 102

Slide 103

Slide 103 text

Questions ? 103