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

Decoupling Flows - Coordinators and Routes

Decoupling Flows - Coordinators and Routes

This presentations shows how we solved navigation problems in Nubank with Coordinators and Routes.

Eduardo Lourenço Pinto Neto

November 11, 2018
Tweet

More Decks by Eduardo Lourenço Pinto Neto

Other Decks in Programming

Transcript

  1. Nubank App Motivação Tela 2 Tela 1 Tela 3 Transição

    do App monolítico para abordagem de produtos Alavancagem do poder das Hyper Media APIs Eram necessários fluxos desacoplados
  2. Nubank App Motivação App Home Cartão de Crédito NuConta Rewa

    UIViewControll er UIViewControll er UIViewControll er Fluxos & Bibliotecas Rotas Transição do App monolítico para abordagem de produtos Alavancagem do poder das Hyper Media APIs Eram necessários fluxos desacoplados
  3. Necessidade • Abrir fluxos de qualquer lugar do app •

    Após separarmos em bibliotecas, não podíamos ter acoplamento • Ser dinâmico e responder ao backend • Poder alterar o comportamento sob demanda
  4. Como resolver? •Um único ponto de entrada •URLs para identificar

    um fluxo •Tudo passado por parâmetro na query •Ainda manter composição •Saber quando uma rota começa, produz efeitos colaterais e termina
  5. Router Fluxo Rotas & Coordinators RoutingHub RouteDescripto r Coordinator UIViewControll

    er UIViewControll er UIViewControll er UIViewControll er Explicação de alto nível - RoutingHub: tudo começa & todos tem acesso - Encontra um RouteDescriptor: Abstraí as necessidades do Coordinator - Coordinator: Maneja o fluxo (apresenta UIViewControllers) - Router: Tudo acontece dentro de um contexto de apresentação
  6. RoutingHub Porta de entrada para abrir um fluxo routingHub .start(url:

    URL(string: "nuapp://domain")) Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Interface: simples start passando uma URL
  7. RoutingHub Porta de entrada para abrir um fluxo routingHub .start(url:

    URL(string: "nuapp://domain")) .subscribe(onNext: { state in //React to state changes } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Podemos ouvir mudanças de estado do fluxo
  8. RoutingHub Porta de entrada para abrir um fluxo routingHub .start(url:

    URL(string: "nuapp://domain")) .subscribe(onNext: { state in //React to state changes }, onError: { error in //Handle flow errors } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Podemos ouvir erros no fluxo
  9. RoutingHub Porta de entrada para abrir um fluxo routingHub .start(url:

    URL(string: "nuapp://domain")) .subscribe(onNext: { state in //React to state changes }, onError: { error in //Handle flow errors }, onCompleted: { //Perform finishing actions }) Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Saber quando o fluxo acabou e fazer algo em resposta
  10. RoutingHub Interface RxSwift para fluxos no app class RoutingHub {

    func start(url: URL) -> Observable<RoutingState> { } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Passos pro RoutingHub Start retorna um Observable<RoutingState>
  11. RoutingHub Interface RxSwift para fluxos no app class RoutingHub {

    func start(url: URL) -> Observable<RoutingState> { guard let router = self.router else { return .error(RoutingError.routerUnavailable) } } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Verifica se tem contexto; Senão, é um erro e podemos jogar pra cima
  12. RoutingHub Interface RxSwift para fluxos no app class RoutingHub {

    func start(url: URL) -> Observable<RoutingState> { guard let router = self.router else { return .error(RoutingError.routerUnavailable) } guard let descriptor = descriptors.first { $0.match(url: url) } else { return .error(RoutingError.unknownRoute) } } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Encontra uma rota que trata a URL; Senão, outro erro que joga pra cima
  13. RoutingHub Interface RxSwift para fluxos no app class RoutingHub {

    func start(url: URL) -> Observable<RoutingState> { guard let router = self.router else { return .error(RoutingError.routerUnavailable) } guard let descriptor = descriptors.first { $0.match(url: url) } else { return .error(RoutingError.unknownRoute) } return router.start(url: url, descriptor: descriptor) } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Delega para o contexto abrir o fluxo
  14. RoutingHub Registro de rotas em Runtime class RoutingHub { var

    router: Router? var descriptors: [RouteDescriptor] = [] func register(descriptor: RouteDescriptor) func register(router: Router) … } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController RoutingHub é o ponto central de roteamento - Mantém as rotas conhecidas - Mantém o contexto atual de apresentação - Registro dinâmico de rotas e contexto
  15. RoutingHub Registro de rotas em Runtime routingHub.register(descriptor: rewardsRoute) routingHub.register(descriptor: chatRoute)

    routingHub.register(descriptor: cardActivationRoute) routingHub.register(descriptor: helpRoute) … routingHub.start(url: URL(string: "nuapp://domain")) Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController - Assim que podemos registrar vários Endpoints - Depois de registrar, podemos só passar a URL
  16. RouteDescriptor Responsável por descrever uma rota e expor seu ciclo

    de vida protocol RouteDescriptor: AnyObject { func match(url: URL) -> Bool func start(url: URL, on context: UIViewController) -> Observable<RoutingState> } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Interface do RouteDescriptor - Match pra dizer que rotas vai tratar - Start pra fazer o trabalho de fato
  17. RouteDescriptor Exemplo class SomeRouteDescriptor: RouteDescriptor { func match(url: URL) ->

    Bool { return url.host == "domain" //Matches xxx://domain } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Como implementar um desses? Primeiro vc escreve a regra de match
  18. RouteDescriptor Exemplo class SomeRouteDescriptor: RouteDescriptor { func match(url: URL) ->

    Bool { return url.host == "domain" //Matches xxx://domain } func start(url: URL, on context: UIViewController) -> Observable<RoutingState> { return Observable.create { (observer) -> Disposable in } } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Exemplo de como envelopar uma rota num Observable Primeiro cria o Observable.create. Lá vamos transformar os eventos do Coordinator nos eventos do Observable
  19. RouteDescriptor Exemplo class SomeRouteDescriptor: RouteDescriptor { func match(url: URL) ->

    Bool { return url.host == "domain" //Matches xxx://domain } func start(url: URL, on context: UIViewController) -> Observable<RoutingState> { return Observable.create { (observer) -> Disposable in let coordinator = SomeCoordinator(context: context) do { try coordinator.start(onCompleted: observer.onCompleted) } catch { observer.onError(error) } } } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController 1. Criamos o coordinator (só quando a rota começa) 2. Iniciamos o fluxo e jogamos erros pra cima
  20. RouteDescriptor Exemplo class SomeRouteDescriptor: RouteDescriptor { func match(url: URL) ->

    Bool { return url.host == "domain" //Matches xxx://domain } func start(url: URL, on context: UIViewController) -> Observable<RoutingState> { return Observable.create { (observer) -> Disposable in let coordinator = SomeCoordinator(context: context) do { try coordinator.start(onCompleted: observer.onCompleted) } catch { observer.onError(error) } return Disposables.create { coordinator.dismiss() } } } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Tratamos o caso de dismiss da rota Quando jogamos o Observable fora, entendemos que não queremos mais a rota.
  21. RoutingState Descrevendo o estado de fluxos func start(url: URL) ->

    Observable<RoutingState> enum RoutingState { } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Como passar o estado da rota? - RoutingState comunica isso
  22. RoutingState Descrevendo o estado de fluxos func start(url: URL) ->

    Observable<RoutingState> enum RoutingState { case preparing } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Estado de Preparing (fazendo request, I/O, computação, etc..) - Tela ainda não foi apresentada
  23. RoutingState Descrevendo o estado de fluxos func start(url: URL) ->

    Observable<RoutingState> enum RoutingState { case preparing case started } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Started: tela já foi apresentada
  24. RoutingState Descrevendo o estado de fluxos func start(url: URL) ->

    Observable<RoutingState> enum RoutingState { case preparing case started case produced } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController Produced: Algo aconteceu na rota que quem chama pode estar interessado
  25. Router Fluxo Coordinator RoutingHub RouteDescripto r Coordinator UIViewControll er UIViewControll

    er UIViewControll er UIViewControll er Abstrair apresentação/passagem de informação entre VCs: Coordinator - Encapsula VCs em uma unidade de fluxo - Ele sabe a lógica de apresentação - Ele sabe passar dados de um VC para o outro
  26. Fluxo Coordinator UIViewController Coordinator UIViewController UIViewController UIViewController Unidade de abstração

    de um fluxo Abstrair apresentação/passagem de informação entre VCs: Coordinator - Encapsula VCs em uma unidade de fluxo - Ele sabe a lógica de apresentação - Ele sabe passar dados de um VC para o outro
  27. Coordinator Coordinator UIViewController Unidade de abstração de um fluxo UIViewController

    UIViewController Abstrair apresentação/passagem de informação entre VCs: Coordinator - Encapsula VCs em uma unidade de fluxo - Ele sabe a lógica de apresentação - Ele sabe passar dados de um VC para o outro
  28. Coordinator Abstração de fluxo class SomeCoordinator { var firstScreen: FirstViewController?

    var secondScreen: SecondViewController? func start(onCompleted: () -> Void) { } func dismiss() { } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController
  29. Coordinator Abstração de fluxo class SomeCoordinator { ... func start(onCompleted:

    () -> Void) { let firstScreen = FirstViewController(nextPressed: { [weak self] userInput in }) self.firstScreen = firstScreen context.present(firstScreen, animated: true, completion: nil) } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController
  30. Coordinator Abstração de fluxo class SomeCoordinator { ... func start(onCompleted:

    () -> Void) { let firstScreen = FirstViewController(nextPressed: { [weak self] userInput in self?.firstScreenFinished(withInput: userInput) }) self.firstScreen = firstScreen context.present(firstScreen, animated: true, completion: nil) } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController
  31. Coordinator Abstração de fluxo class SomeCoordinator { ... func start(onCompleted:

    () -> Void) { let firstScreen = FirstViewController(nextPressed: { [weak self] userInput in self?.firstScreenFinished(withInput: userInput) }) self.firstScreen = firstScreen context.present(firstScreen, animated: true, completion: nil) } func firstScreenFinished(withInput input: String) { let secondScreen = SecondViewController(previousInput: input, nextPressed: { userInput in /*…*/ }) self.secondScreen = secondScreen firstScreen?.present(secondScreen, animated: true, completion: nil) } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController
  32. Coordinator Abstração de fluxo class SomeCoordinator { ... func start(onCompleted:

    () -> Void) { let firstScreen = FirstViewController(nextPressed: { [weak self] userInput in self?.firstScreenFinished(withInput: userInput) }) self.firstScreen = firstScreen context.present(firstScreen, animated: true, completion: nil) } func firstScreenFinished(withInput input: String) { let secondScreen = SecondViewController(previousInput: input, nextPressed: { userInput in /*…*/ }) self.secondScreen = secondScreen firstScreen?.present(secondScreen, animated: true, completion: nil) } func dismiss() { firstScreen?.dismiss(animated: true, completion: nil) } } Router Fluxo RoutingHu b RouteDescript or Coordinator UIViewController UIViewController UIViewController UIViewController
  33. Router Fluxo Rotas & Coordinators RoutingHub RouteDescripto r Coordinator UIViewControll

    er UIViewControll er UIViewControll er UIViewControll er Explicação de alto nível - RoutingHub: tudo começa & todos tem acesso - Encontra um RouteDescriptor: Abstraí as necessidades do Coordinator - Coordinator: Maneja o fluxo (apresenta UIViewControllers) - Router: Tudo acontece dentro de um contexto de apresentação