Slide 1

Slide 1 text

Architecting Android and iOS apps in 2020

Slide 2

Slide 2 text

Agenda 1 Problems building apps today Architecture - that solves problems 2 Implementation 3 Recap 4

Slide 3

Slide 3 text

Problems building apps today * Managing UI State is hard Debugging is hard * Testing is hard * Building on both takes time! * 1

Slide 4

Slide 4 text

Activity ViewModel SEARCH OMDB api fun onRestoreFromHistory() 2SearchMovieResult fun onAddToHistory() 2SearchHistoryResult fun onSearchMovie() : SearchMovieResult Managing UI State is hard *

Slide 5

Slide 5 text

DashboardActivity DashboardViewModel Managing UI State is hard *

Slide 6

Slide 6 text

Problems building apps today * Managing UI State is hard 1 Existing architectures don’t scale with complexity !!!

Slide 7

Slide 7 text

Problems building apps today * Managing UI State is hard Debugging is hard * Testing is hard * Building on both takes time! * 1

Slide 8

Slide 8 text

Agenda 1 Problems building apps today Architecture - that solves problems 2 Implementation 3 Recap 4

Slide 9

Slide 9 text

Agenda 1 Problems building apps today Architecture - that solves problems 2 Guiding principles for an architecture Use MVC, MVP, MVVM, MVI or whatever your ❤ pleases !

Slide 10

Slide 10 text

Agenda 1 Problems building apps today Guiding principles for an architecture 2 Implementation 3 Recap 4

Slide 11

Slide 11 text

Guiding principles for an architecture * * 2 Model your UI as a single function Avoid mutating state * Test at the business level not implementation level

Slide 12

Slide 12 text

Model your UI as a single function * What is a function?

Slide 13

Slide 13 text

ViewModel SEARCH fun onRestoreFromHistory() 2SearchMovieResult fun onAddToHistory() 2SearchHistoryResult fun onSearchMovie() : SearchMovieResult Activity or View Controller

Slide 14

Slide 14 text

ViewModel SEARCH Activity or View Controller input output Modeling UI as a function

Slide 15

Slide 15 text

Agenda 1 Problems building apps today Guiding principles for an architecture 2 Implementation 3 Recap 4

Slide 16

Slide 16 text

Implementation What is a function?

Slide 17

Slide 17 text

ViewModel Activity or View Controller SEARCH input (View)Event

Slide 18

Slide 18 text

(View)Event Activity or View Controller SEARCH input sealed class ViewEvent { object ViewResume : ViewEvent() data class SearchMovie( val searchedMovieTitle: String = “" ) : ViewEvent() data class AddToHistory( val searchedMovie: MovieSearchResult ) : ViewEvent() } enum ViewEvent { case viewResume case searchMovie(String) case addToHistory(MovieSearchResult) }

Slide 19

Slide 19 text

ViewModel (View)Event private val viewEventSubject: PublishSubject = PublishSubject.create() Activity or View Controller SEARCH input viewEventSubject.onNext(event) fun processInput( event: ViewEvent ) { }

Slide 20

Slide 20 text

ViewModel (View)Event private val viewEventSubject: PublishSubject = PublishSubject.create() Activity or View Controller SEARCH input fun processInput( event: ViewEvent ) { viewEventSubject.onNext(event) } func processInput( event: ViewEvent ) { viewEventSubject.onNext(event) } private let viewEventSubject: PublishSubject = .init()

Slide 21

Slide 21 text

SEARCH (View)State output ViewModel

Slide 22

Slide 22 text

data class ViewState( val movieTitle: String = "", val moviePosterUrl: String?, val genres: String = "", val plot: String = "", val rating: String = "", val searchHistoryList: List = emptyList() ) SEARCH (View)State output

Slide 23

Slide 23 text

data class ViewState( val movieTitle: String = "", val moviePosterUrl: String?, val genres: String = "", val plot: String = "", val rating: String = "", val searchHistoryList: List = emptyList() ) SEARCH (View)State struct ViewState: Equatable { let movieTitle: String let moviePosterUrl: String? let genres: String let plot: String let rating: String let rating22 String let searchHistoryList: [MovieSearchResult] }

Slide 24

Slide 24 text

SEARCH (View)State output ViewModel let viewState: Observable

Slide 25

Slide 25 text

SEARCH (View)State le Activity or View Controller viewModel.viewState .subscribeOn( ConcurrentDispatchQueueScheduler( qos: .background) ) .observeOn(MainScheduler.instance) .subscribe( onNext: { [weak self] vs in posterImageView.load( imageUrl: vs.moviePosterUrl) movieTitleView.text = vs.movieTitle genreInfoView.text = vs.genres plotSummaryView.text = vs.plot srRating1.text = vs.rating1 srRating2.text = vs.rating2 }, onError: { err in print(" we got an error \(err)") } ) .disposed(by: dbag)

Slide 26

Slide 26 text

SEARCH (View)State le Activity or View Controller viewModel.viewState .subscribeOn( ConcurrentDispatchQueueScheduler( qos: .background) ) .observeOn(MainScheduler.instance) .subscribe( onNext: { [weak self] vs in posterImageView.load( imageUrl: vs.moviePosterUrl) movieTitleView.text = vs.movieTitle genreInfoView.text = vs.genres plotSummaryView.text = vs.plot srRating1.text = vs.rating1 srRating2.text = vs.rating2 }, onError: { err in print(" we got an error \(err)") } ) .disposed(by: dbag)

Slide 27

Slide 27 text

ViewModel Activity or View Controller SEARCH input: ViewEvent

Slide 28

Slide 28 text

ViewModel Activity or View Controller SEARCH output: ViewState

Slide 29

Slide 29 text

ViewModel Activity or View Controller SEARCH Subject Observable

Slide 30

Slide 30 text

Subject Observable viewEventSubject .flatMap { event `a when (event) { is ViewResume `a { Observable.just(startingViewState) } is SearchMovie `a { doNetworkRequest(event) .map { result `a ntwkResultToViewState(result) } } `b … }

Slide 31

Slide 31 text

viewEventSubject .flatMap { event `a when (event) { is ViewResume `a { Observable.just(startingViewState) } is SearchMovie `a { doNetworkRequest(event.movieSearched) .map { movie `a ntwkResultToViewState(movie) } } `b … } Subject Observable

Slide 32

Slide 32 text

viewEventSubject .flatMap { event `a when (event) { is ViewResumeEvent `a { Observable.just(startingViewState) } is SearchMovieEvent `a { doNetworkRequest(event.movieSearched) .map { movie `a ntwkResultToViewState(movie) } } `b … } Subject Observable

Slide 33

Slide 33 text

SEARCH ViewResume Full vs vs.toolbarTitle vs.searchDetails Full vs + vs.searchHistory + Subject Observable

Slide 34

Slide 34 text

SEARCH ViewResume Full vs vs.toolbarTitle vs.searchDetails Full vs + vs.searchHistory + Nil Nil Subject Observable

Slide 35

Slide 35 text

SEARCH ViewResume SearchMovie vs.searchDetails Full vs vs.toolbarTitle + + vs.searchHistory Nil Subject Observable

Slide 36

Slide 36 text

Full vs vs.toolbarTitle + + SEARCH ViewResume SearchMovie AddToHistory vs.searchDetails vs.searchHistory Subject Observable

Slide 37

Slide 37 text

viewEventSubject .flatMap { event `a when (event) { is ViewResumeEvent `a { Observable.just(startingViewState) } is SearchMovieEvent `a { doNetworkRequest(event) .map { result `a ntwkResultToViewState(result) } } `b … } Subject Observable

Slide 38

Slide 38 text

viewEventSubject .flatMap { event `a when (event) { is ViewResumeEvent `a { Observable.just(startingViewState) } is SearchMovieEvent `a { doNetworkRequest(event) .map { result `a ntwkResultToViewState(result) } } `b … } Subject Observable

Slide 39

Slide 39 text

viewEventSubject .flatMap { event `a when (event) { is ViewResumeEvent `a { Observable.just(startingViewState) } is SearchMovieEvent `a { doNetworkRequest(event) .map { result `a ntwkResultToViewState(result) } } `b … } Subject Observable

Slide 40

Slide 40 text

var prevVS = ViewState() var newVs = ntwkResultToViewState(result) prevVS.movieTitle = newVs.movieTitle prevVS.moviePosterUrl = newVs.moviePosterUrl `b ``c prevVS Option 1 viewEventSubject .flatMap { event `a when (event) { is ViewResumeEvent `a { Observable.just(startingViewState) } is SearchMovieEvent `a { doNetworkRequest(event) .map { result `a ntwkResultToViewState(result) } } `b … } Subject Observable

Slide 41

Slide 41 text

.scan

Slide 42

Slide 42 text

{ previousVS2 ViewState, (ViewState()) event : ViewEvent `a event `a gives new view state previousVS `a take what you need here Send down (event + previousVS) .scan Option 2 Subject Observable

Slide 43

Slide 43 text

{ previousVS2 ViewState, (ViewState()) event : ViewEvent `a event `a gives new view state previousVS `a take what you need here Send down (event + previousVS) .scan viewEventSubject Subject Observable

Slide 44

Slide 44 text

event `a gives new view state previousVS `a take what you need here Send down (event + previousVS) .scan viewEventSubject { previousVS2 ViewState, event : ViewEvent `a (ViewState()) when (event) { is ViewResumeEvent `a `b… is SearchMovieEvent `a `b… `b … } } Subject Observable

Slide 45

Slide 45 text

.scan viewEventSubject { previousVS2 ViewState, event : ViewEvent `a (ViewState()) when (event) { is ViewResumeEvent `a `b… is SearchMovieEvent `a `b… `b … } } event `a gives new view state previousVS `a take what you need here Send down (event + previousVS) Subject Observable

Slide 46

Slide 46 text

What about one-time ViewState changes ? Implementation

Slide 47

Slide 47 text

Implementation 1 Toast notifications Alert dialogs 2 Navigation actions 3 Non-persisting UI changes 4 What about one-time ViewState changes ?

Slide 48

Slide 48 text

Implementation 1 Toast notifications Alert dialogs 2 Navigation actions 3 Non-persisting UI changes 4 (View)State (View)Effects + What about one-time ViewState changes ?

Slide 49

Slide 49 text

.share() (View)Result ViewEvent * State flow ViewState (View)Effects New Data structure Private to VM

Slide 50

Slide 50 text

.share() ViewState (View)Result (View)Effects ViewEvent viewEventSubject

Slide 51

Slide 51 text

.share() ViewState (View)Result (View)Effects ViewEvent .flatmap { event `a Observable in } viewEventSubject enum ViewResult { case viewResumeResult case searchMovieResult( movieResult: Movie loading: Bool, error: Error? ) `b … }

Slide 52

Slide 52 text

viewEventSubject switch(event) { case .viewResume: return Observable .just(.viewResumeResult) case .searchMovie(let movieTitle)2 return doNetworkRequest(movieTitle) `b other event cases … } .flatmap { event `a Observable in } .share() ViewState (View)Result (View)Effects ViewEvent

Slide 53

Slide 53 text

.flatmap { event `a Observable in } viewEventSubject switch(event) { `b other event cases … } .share() .share() ViewState (View)Result (View)Effects ViewEvent

Slide 54

Slide 54 text

.share() .share() ViewState (View)Result (View)Effects ViewEvent private let viewResults: Observable = .flatmap { event `a Observable in } viewEventSubject switch(event) { `b other event cases … }

Slide 55

Slide 55 text

.share() .share() ViewState (View)Result (View)Effects private let viewResults: Observable = `b … viewResults .scan(startingViewState) { previousVS, viewResult in switch viewResult { case .screenLoadResult: return previousVS.copy() case .searchMovieResult(let movie)2 return previousVS + movie } let viewState: Observable =

Slide 56

Slide 56 text

.share() .share() ViewState (View)Result (View)Effects private let viewResults: Observable = `b … viewResults .map { viewResult in switch viewResult { case .screenLoadResult: return .noEffect case .searchMovieResult(let movie)2 return .showNotificationToast } let viewEffects: Observable =

Slide 57

Slide 57 text

.share() ViewState (View)Result (View)Effects let viewEffects: Observable = let viewState: Observable = viewModel.viewState .subscribeOn( ConcurrentDispatchQueueScheduler( qos: .background) ) .observeOn(MainScheduler.instance) .subscribe( onNext: { [weak self] vs in posterImageView.load( imageUrl: vs.moviePosterUrl) movieTitleView.text = vs.movieTitle genreInfoView.text = vs.genres plotSummaryView.text = vs.plot }, onError: { err in print(" we got an error \(err)") } ) .disposed(by: dbag) Activity or View Controller

Slide 58

Slide 58 text

.share() ViewState (View)Result (View)Effects let viewEffects: Observable = let viewState: Observable = viewModel.viewEffects .subscribeOn( ConcurrentDispatchQueueScheduler( qos: .background) ) .observeOn(MainScheduler.instance) .subscribe( onNext: { [weak self] ve in case .showNotificationToast: self`fbanner`fshow() case .noEffect: print("no effect") }, onError: { err in print(" we got an error \(err)") } ) .disposed(by: dbag) Activity or View Controller

Slide 59

Slide 59 text

.share() ViewState ViewResult ViewEffects ViewEvent Activity or View Controller ViewModel

Slide 60

Slide 60 text

Movie Search Demo App github.com/kaushikgopal/movies-usf-ios github.com/kaushikgopal/movies-usf-android

Slide 61

Slide 61 text

Tests `b given: movie "Blade Runner 2049" exists `b when : searching for movie "blade runner 2049" `b then : show result @Test fun onSearchingMovie_shouldSeeSearchResults() { viewModel = MovieSearchVM(mockApp, mockMovieRepo) val viewStateTester = viewModel.viewState.test() viewModel.processInput(SearchMovieEvent("blade runner 2049")) viewStateTester.assertValueAt(1) { assertThat(it.movieTitle).isEqualTo("Searching Movie``c") true } viewStateTester.assertValueAt(2) { assertThat(it.movieTitle).isEqualTo("Blade Runner 2049") assertThat(it.moviePosterUrl) .isEqualTo("https:`bmedialamz.com/images/M/MV5_V1_SX300.jpg") assertThat(it.rating1).isEqualTo("\n8.1/10 (IMDB)\n87% (RT)") true } }

Slide 62

Slide 62 text

`b given: movie "Blade" exists `b when : searching for movie "Blade" `b then : show result func test_movieBladeExists_searchingForIt_showMovieResult() { let viewModel = MovieSearchVM(FakeMovieSearchService()) let vsObserver = scheduler.createObserver( MovieSearchVM.ViewState.self ) viewModel.viewState .subscribe(vsObserver) .disposed(by: dbag) scheduler.scheduleAt(0) { viewModel.processViewEvent( event: MovieSearchVM.ViewEvent.screenLoad ) } scheduler.scheduleAt(1) { viewModel.processViewEvent( event: MovieSearchVM.ViewEvent.searchMovie("Blade")) } scheduler.start() let vs12 MovieSearchVM.ViewState = Tests

Slide 63

Slide 63 text

viewModel.processViewEvent( event: MovieSearchVM.ViewEvent.screenLoad ) } scheduler.scheduleAt(1) { viewModel.processViewEvent( event: MovieSearchVM.ViewEvent.searchMovie("Blade")) } scheduler.start() let vs12 MovieSearchVM.ViewState = vsObserver.events .filter { $0.time `v 1 } .compactMap { $0.value.element } .last! XCTAssertEqual(vs1.movieTitle, "Blade") XCTAssertEqual(vs1.genres, "Action, Horror, Sci-Fi") XCTAssertEqual( vs1.moviePosterUrl, "https:`bm.medialamz.com/images/M/V1_SX300.jpg" ) XCTAssertEqual(vs1.plot, "A halflvampire, halflmortal…") XCTAssertEqual(vs1.rating1, "IMDB : 7.1/10") XCTAssertEqual(vs1.rating2, "Rotten T2 54%") } Tests

Slide 64

Slide 64 text

That’s all folks !!! 1 Resources 2 Guided Q&A

Slide 65

Slide 65 text

Resources 1 Movie Search Demo App github.com/kaushikgopal/movies-usf-ios github.com/kaushikgopal/movies-usf-android @kaushikgopal blog.kaush.co fragmentedpodcast.com We're hiring mobile devs! tech.instacart.com #148 #151

Slide 66

Slide 66 text

Guided Q&A 1 What about frameworks like Formula/MVRx/Mobius? 2 What about things like Navigation/Analytics? 3 Is testing and debugging really much easier? 4 Does this handle activity rotation, savedInstanceState? 5 Any disadvantages or shortcomings of this approach? 6 Is performance a problem if you have many viewState emissions? 7 What if setting a ViewState causes a ViewEvent to be triggered? 8 What if I need to emit multiple ViewEffects for one ViewEvent?