Share
Embed
2 years of Redux in iOS. Lessons learned
DAloG
July 15, 2017
Transcript
58 slides
-
ALEXEY DEMEDECKIY @DALOOG > 7 years of iOS development >
Early adopter > Strong typed person > Semantic oriented programmer -
DATA-DRIVEN ARCHITECTURES > Flux > Redux > Elm > MobX
> Cycle.js ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
> Single source of truth > Single place of change
> Single way to update ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
...debugging is twice as hard as writing a program in
the first place. — Brian Kernighan (1985) ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
LESSONS: 1. Data-driven VC 2. Correct ViewModels 3. Data-driven vs
event-driven 4. Managed side effects 5. High level testing ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
TRADITIONAL MVC class ViewController: UIViewController { var modelID: String? var
api: APIClient? var database: DatabaseWrapper? } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
TRADITIONAL MVC > intermediate state > hard to track changes
> fragile UI ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
REACTIVE MVVM class ViewModel { var name: Observable<String> var age:
Observable<String> func birthday() {} func mariage() {} } class ViewController: UIViewController { var viewModel: Observable<ViewModel> } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
REACTIVE MVVM > transient states > sharing view models >
reactive paradigm ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
DATA-DRIVEN VC class ViewController: UIViewController { struct ViewModel { let
name: String let age: String let birthday: () -> () let marry: () -> () } var viewModel: ViewModel { didSet { setNeedsLayout() } } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
Lesson #1: EVERY VIEW CONTROLLER SHOULD BE DATA-DRIVEN ALEXEY DEMEDECKIY
(@DALOOG) FOR #MOCONF -
LOGGING IN struct ViewModel { let isLoggedIn: Bool let isLoading:
Bool let login: (Username, Password) -> () let logout: () -> () } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
LOGGING IN enum ViewModel { case loginForm(login: (Username, Password) ->
()) case loading case loggedIn(logout: () -> ()) } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
REAL LIFE EXAMPLE public enum Props { case noPlayer, player(Player),
pictureInPicture public struct Player { public var playlist: Playlist? public struct Playlist { public var next: Action<Void>? public var prev: Action<Void>? } public var item = Item.nonplayable("") public enum Item { case playable(Controls) case nonplayable(String) } } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
LOGGING IN struct UIProps { let loginButtonAction: () -> ()
let loginButtonEnabled: Bool let spinnerHidden: Bool let logoutButtonAction: () -> () let logoutButtonEnabled: Bool init(viewModel: ViewModel) { ... } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
REAL LIFE EXAMPLE struct UIProps { var loading: Bool ...
init(props: Props) { loading = { guard case .player(let player) = props else { return false } guard case .playable(let props) = player.item else { return false } return props.loading }() } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
BEST OF BOTH WORLDS > Correctness of state > Simplicity
of rendering > Tesatbility ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
Lesson #2: CORRECTNESS OF VALUES SHOULD BE ENFORCED WITH TYPES
ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
EVENTS ARE FRAGILE > Order is crucial > Context is
important > Errors are cumulative ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
DATA IS BETTER THAN EVENT > No order constraints >
You can miss some data > Eventually ok ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
REAL LIVE EXAMPLE public struct AVPlayerProps { public let url:
URL public let isPlaying: Bool public let isMuted: Bool public let newTime: CMTime? public let callbacks: PlayerObserver.Callbacks public let didPlayToTime: Action<CMTime> } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
EVENT SOURCES > external systems > side effects > user
input ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
DATA SOURCE class Application<Model, Event> { var model: Observable<Model> init(from
model: Model) { ... } func apply(event: Event) { ... } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
SINGLETONS class APIClient { static let shared = APIClient() private
init() { ... } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
SINGLETONS > Unclear usage > Shared state > Miss of
context ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
DI protocol APIClient { ... } class Service { var
apiClient: APIClient } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
DI > Hard to manage > Transient dependencies > API
breakage ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
DI func login(username: Username, password: Password) { self.apiClient.get( path: "/session",
params: ["useraname": useraname, "password": password]) { error, value in if let error = error { return self.showAlert(error) } return self.pushUserPage(value!) } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
EFFECTS enum Effect { case none case get(Path, Params, (Error?,
Value?) -> Effect) case showAlert(Error) case pushUserPage(Value) } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
EFFECTS func login(username: Username, password: Password) -> Effect { let
params = ["username": username, "password": password] return .get("/session", params) { error, value in if let error = error { return .showError(error) } if let value = value { return .pushUserPage(value) } return .none } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
EFFECTS class Application<Model, Event> { var model: Observable<Model> init(from model:
Model) { ... } func apply(event: Event) { ... } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
EFFECTS class Application<Model, Event, Effect> { ... func perform(effect: Effect)
{ ... } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
UNIT TESTS IN DATA-DRIVEN > Easy to start > Low
support cost > Show nothing ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
USUAL INTEGRATION TESTS > Hard to write > Requires constant
attention > Covers only critical sections ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
DATA-DRIVEN INTEGRATION TESTS class Application<Model, Event, Effect> { var tracer:
Tracer<Model, Event, Effect> var model: Model { didSet { tracer.record(model: model) } } func apply(event: Event) { tracer.record(event) } func perform(effect: Effect) { tracer.record(effect) } } ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
RESULTS > events.json - 3MB > model.json - 35MB >
effects.json - 4MB ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
VERIFICATION TESTING > Record real usage scenarios > Verify consistency
> Discover side effects ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
1. Data-driven VC 2. Correct ViewModels 3. Data-driven vs event-driven
4. Managed side effects 5. High level testing ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
USEFUL LINKS > Idiomatic Redux > Video Player Controls >
Video Renderer > Elm ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
COMMUNITY EFFORTS > Swift Frameworks by @inamiy > Statements, messages
and reducers by @cocoawithlove > Reducers by @chriseidhof ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
KATANA > Virtual UI > Data-driven layout > Strong foundation
ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
PORTAL BY @GUIDOMB > Elm inspired > Managed side effects
> Data-driven layout ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF -
ADVANCED TOPICS > Cross-platform core (JSCore) > Tooling (Redux Dev
Tools) > Adopting in teams ALEXEY DEMEDECKIY (@DALOOG) FOR #MOCONF