Engenharia iOS

[Arquitetura iOS #9] Resumo do ReactorKit: o padrão de arquitetura unidirecional

ReactorKit é um framework que tornou o fluxo de dados unidirecional um padrão de fato na combinação UIKit e RxSwift. Este artigo resume os papéis de View e Reactor, por que a etapa intermediária Mutation é necessária e as diferenças em relação ao TCA.

4 min de leitura
Imagem de capa de [Arquitetura iOS #9] Resumo do ReactorKit: o padrão de arquitetura unidirecional

No artigo sobre TCA(The Composable Architecture), abordamos o fluxo de dados unidirecional. Na verdade, a cena iOS coreana já contava com um framework que havia tornado o unidirecional um padrão: o ReactorKit.

Publicado por Suyeol Jeon em 2017, esse framework tornou-se a arquitetura de fato para a combinação UIKit + RxSwift após ser adotado pela StyleShare e por serviços ligados à Kakao. Houve até uma época em que “experiência com ReactorKit é um diferencial” aparecia com frequência nas vagas.

Hoje, vamos revisar a estrutura do ReactorKit, suas diferenças em relação ao TCA e sua posição atual.


View e Reactor: são apenas dois papéis

A estrutura do ReactorKit é simples. Cada tela é dividida em View e Reactor.

  • View: envia a entrada do usuário para Action e assina State para renderizar a tela. Não contém lógica.
  • Reactor: um bloco de lógica que recebe Actions e produz State. Ele não conhece a UI.

O ponto central é que o fluxo de dados dentro do Reactor fica fixo em uma única direção.

Action → mutate() → Mutation → reduce() → State → View

Quando um toque no botão entra como Action.refresh, mutate() trata efeitos colaterais, como requisições de rede, e emite Mutation.setItems([...]). reduce() recebe essa Mutation e calcula um novo State. A View assina esse State e atualiza a tela.

Por que a etapa intermediária Mutation é necessária?

Se você conhece MVVM(Model-View-ViewModel), pode pensar: “Por que não alterar o State diretamente a partir da Action?”. A Mutation existe para isolar o trabalho assíncrono.

mutate() é o único lugar onde operações assíncronas são permitidas. Já reduce() é uma função pura: com o mesmo State e Mutation, sempre produz o mesmo resultado. Como os efeitos colaterais ficam confinados a um só lugar, também há um único ponto para investigar ao rastrear por que o estado mudou.

final class SearchReactor: Reactor {
    enum Action { case updateQuery(String) }
    enum Mutation { case setResults([Repo]) }

    struct State { var results: [Repo] = [] }

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .updateQuery(let query):
            return service.search(query)
                .map { Mutation.setResults($0) }
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .setResults(let repos): newState.results = repos
        }
        return newState
    }
}

Reactor é lógica pura que não conhece a UI, então os testes são simples. Envie uma Action e verifique o State.

Diagrama do fluxo unidirecional Action mutate Mutation reduce State no ReactorKit
O trabalho assíncrono é isolado em mutate(), e os cálculos puros em reduce()

Qual é a diferença em relação ao TCA?

Ambos são arquiteturas unidirecionais influenciadas pelo Redux, mas diferem na unidade de aplicação.

ReactorKit TCA
Base RxSwift Combine·Swift Concurrency
Unidade de aplicação Um Reactor por tela (View) Composição de Reducers por funcionalidade em uma árvore
Forma de adoção Introdução gradual, uma tela por vez Estruturação do app inteiro como um único sistema
Principal contexto UIKit SwiftUI

A praticidade do ReactorKit vem da forma de adoção. Em um projeto MVC(Model-View-Controller) existente, você pode escolher uma única tela e adicionar um Reactor. Não é preciso reescrever o app inteiro. Já o TCA gerencia também a composição de estado entre funcionalidades, mas exige adotar o sistema como um todo.

Também existiu uma biblioteca chamada ReSwift, uma adaptação direta do Redux para iOS, mas o modelo de store único em todo o app era difícil de combinar com UIKit e não se espalhou tanto quanto o ReactorKit.

O ReactorKit nos dias de hoje

Sendo sincero, menos projetos novos estão escolhendo o ReactorKit. Suas limitações ficaram mais evidentes.

Primeiro, ele é fortemente acoplado ao RxSwift. Com a migração do ecossistema da Apple para Combine e Swift Concurrency, a própria dependência do RxSwift virou um peso.

Segundo, ele não combina bem com SwiftUI. O protocolo View e o modelo de binding foram projetados com base nas atualizações imperativas do UIKit.

Ainda assim, o legado do ReactorKit é claro. Foi o primeiro framework a transmitir aos desenvolvedores iOS coreanos a noção de que “o estado flui em uma única direção” e “os efeitos colaterais devem ser isolados em um único lugar”. Para quem está aprendendo TCA agora, a estrutura Action·Mutation·State provavelmente não parece estranha; boa parte dessa intuição foi formada na era do ReactorKit.

Se você mantém uma base de código existente com UIKit + RxSwift, o ReactorKit continua sendo uma escolha comprovada. Como ele é integrado e removido por tela, migrar depois, retirando-o tela a tela, também é relativamente simples.

Ilustração de revezamento passando o bastão do fluxo unidirecional do ReactorKit para o TCA
O ReactorKit plantou primeiro a noção de unidirecionalidade, e o TCA a levou adiante

Resumo

  • ReactorKit é uma arquitetura baseada em RxSwift que divide cada tela entre View e Reactor e impõe o fluxo unidirecional Action → Mutation → State.
  • O trabalho assíncrono é isolado em mutate(), e o cálculo puro do estado em reduce(). Por isso, rastrear e testar é fácil.
  • Os conceitos são iguais aos do TCA, mas seu diferencial é a adoção gradual e prática por tela.
  • A adoção em projetos novos diminuiu por causa do acoplamento ao RxSwift e da incompatibilidade com SwiftUI, mas ele continua válido em código legado de UIKit.

No próximo artigo, vamos abordar o RIBs da Uber, que divide o app por unidades de lógica de negócio, e não por telas. Vamos analisar de verdade a arquitetura que foi mencionada em apenas um parágrafo no artigo sobre VIPER.


Referências

Continue lendo