Este artigo foi publicado no equinociOS, promovido pelo Cocoaheads-Br.
Muitos desenvolvedores, quando escutam falar de programação funcional e programação reativa, assumem uma postura defensiva e a conversa tende a não sair do lugar. “Mas o meu aplicativo já está 50% desenvolvido, não vale a pena mudar a arquitetura dele agora”, ou então “Eu só estou dando manutenção neste aplicativo, não tenho tempo para alterar a arquitetura dele”, ou ainda ” Não posso mudar a arquitetura do projeto, porque ninguém mais da equipe sabe essa magia negra aí” são frases frequentes que vamos encontrar ao tentar sugerir a adoção desses paradigmas.
Mas o que esses desenvolvedores não sabem é que programação funcional/reativa não necessita que você altere a arquitetura do seu projeto.
A programação funcional tem muito mais a ver com paradigmas a serem utilizados do que com frameworks e arquiteturas de projetos.
Mas obviamente existem diversas arquiteturas e frameworks que facilitam a adoção desses paradigmas, porém elas não são obrigatórias e você pode, sim, começar hoje mesmo a inserir códigos funcionais e reativos no seu projeto.
Neste artigo, vamos fazer extenso uso de RxSwift, uma biblioteca de programação Reativa.
Agora, caso este seja o seu primeiro contato com essa biblioteca, ou você ainda não se sinta confortável com os paradigmas funcionais, eu recomendo que você leia “RxSwift: Como vim parar aqui”, do Bruno Koga. Ou ainda, visite a documentação do projeto.
Fazendo mais com menos
Bom, essa é a ideia deste artigo. Mostrar como podemos utilizar o paradigma da programação funcional, junto com RxSwift e com uma pitada de Generics para tornar o seu código mais reativo e funcional.
E para isso eu desenvolvi um aplicativo de gerenciamento de despesas (GitHub) para ilustrar alguns pontos que quero abordar. Eu sugiro que você baixe o código, dê uma olhada no projeto, brinque um pouco com o aplicativo e depois volte para continuarmos.
Bom, agora que você voltou podemos começar…
Sobre a arquitetura do projeto, além do RxSwift para a programação reativa, estou utilizando o Realm para gerenciar o banco de dados local, o Hue para o gerenciamento de cores, o SnapKit para usar autolayout no código e o NibDesignable para gerenciar as telas e deixar mais leve nosso storyboard.
Um dos recursos mais interessantes do Realm é a lista com live update, que permite que façamos uma query no banco, que está sempre atualizada, incluindo atualizações que transações futuras podem efetuar na base, facilitando manter nossa UI sempre fresca e atualizada.
Porém o Realm não tem suporte nativo para RxSwift. No entanto, é muito simples criar nossas próprias sequências de dados (Observables, como são chamados no RxSwift) e podemos nós mesmos adicionar esse suporte.
@fpillet (um usuário bem ativo da comunidade de RxSwift) fez um gist adicionando esse recurso ao Realm, e nós vamos utilizar esse gist no nosso projeto.
Bom vamos ao código:
Talk is cheap. Show me the code – Torvalds, Linus
import UIKit import RealmSwift import RxSwift struct AppState { private let disposeBag = DisposeBag() static let current = AppState() let currentCategory = Variable<ExpenseCategory?>(nil) let entries = Variable<[Expense]>([]) let allEntries: Observable<[Expense]> let allExpenses: Observable<[Expense]> let currentExpensesTotal: Observable<Double> let currentTintColor: Observable<UIColor> private init() { let observableCategory = self.currentCategory.asObservable() observableCategory.map { (category) -> Int in guard let selectedCategory = category else { return -1 } return selectedCategory.rawValue }.distinctUntilChanged().map { (categoryNumber) -> Observable<[Expense]> in let realm = try! Realm() var results = realm.objects(Expense) if categoryNumber >= 0 { results = results.filter("_category == \(categoryNumber)") } results = results.sorted("date", ascending: false) return results.asObservableArray() }.switchLatest().bindTo(self.entries).addDisposableTo(disposeBag) self.allEntries = self.entries.asObservable() self.allExpenses = self.allEntries.map { (expenses) -> [Expense] in return expenses.filter { expense in return expense.type == ExpenseType.Outcome } } self.currentExpensesTotal = self.allExpenses.map { (list) -> Double in return list.reduce(0.0, combine: { (total, expense) -> Double in return total + expense.amount }) } self.currentTintColor = observableCategory.map { (category) -> UIColor in return UIColor.hex(category?.tintColor ?? ExpenseCategory.allColors) } } }
Basicamente esse é um objeto que representa o estado atual do aplicativo. Ele é responsável por rastrear as mudanças no banco de dados e expor os valores já mapeados para serem consumidos pelas outras classes e funções. Vamos então analisar as partes deste código:
Mantendo sempre o “estado atual” atualizado…
let currentCategory = Variable<ExpenseCategory?>(nil) let entries = Variable<[Expense]>([]) let allEntries: Observable<[Expense]> let allExpenses: Observable<[Expense]> let currentExpensesTotal: Observable<Double> let currentTintColor: Observable<UIColor>
Essas são as propriedades disponíveis para serem observadas, e basicamente todas são dependentes de currentCategory. Quando a categoria atual é alterada, todas as outras propriedades também são atualizadas. Conseguimos esse feito com o seguinte código:
let observableCategory = self.currentCategory.asObservable() observableCategory.map { (category) -> Int in guard let selectedCategory = category else { return -1 } return selectedCategory.rawValue }.distinctUntilChanged().map { (categoryNumber) -> Observable<[Expense]> in let realm = try! Realm() var results = realm.objects(Expense) if categoryNumber >= 0 { results = results.filter("_category == \(categoryNumber)") } results = results.sorted("date", ascending: false) return results.asObservableArray() }.switchLatest().bindTo(self.entries).addDisposableTo(disposeBag)
Primeiro estamos mapeando a categoria selecionada(ExpenseCategory) para um Int (caso nenhuma categoria esteja selecionada, mapeamos o resultado para -1). Por que um Int? Porque esse é o valor mapeado no banco de dados do Realm.
Mas nós não queremos atualizar a query quando selecionarmos uma nova categoria que seja igual à categoria atual (a nossa lista já é autoatualizável, não precisamos fazer nada aqui). Essa é a função do distinctUntilChanged. Ele filtra resultados que sejam iguais ao último enviado.
Depois, utilizamos esse Int para fazer nossa query no banco de dados (“_category == \(categoryNumber)”) e, utilizando aquele gist, retornamos uma sequência observável desses registros encontrados pela nossa query.
Perceba que nesse passo estamos gerando um Observable que produz um Observable que gera uma lista de Expenses (<[Expense]>). Confuso? A princípio pode parecer que sim, mas, para tentar entender, vamos fazer uma analogia com uma impressora 3D. Pense que um Observable é como uma impressora 3D especializada em imprimir um objeto de um tipo E. No nosso caso, nós estamos criando uma impressora 3D que imprime outra impressora 3D especializada em imprimir uma lista de E (é como se toda vez que mudamos self.currentCategory, criamos uma nova impressora 3D que imprime os itens). Mas nós estamos interessados em observar a lista de Expenses, e não as impressoras que as geram. E como fazemos isso? Utilizando o switchLatest. Com esse operador, nós utilizamos os resultados sempre da última impressora que imprime a lista de itens que for gerada pela nossa impressora matriz. Com isso, nós criamos uma impressora que imprime uma lista de E, ou Observable<[E]>.
Assim: Observable<Observable<[Expense]>>.switchLatest = Observable<[Expense]>.
Espero que essa analogia tenha facilitado um pouco as coisas.
E por fim salvamos uma referência a esse Observable em self.entries. Com isso, toda vez que self.currentCategory receber um novo valor distinto do anterior, iremos refazer nossa query e, com isso, atualizar a variável self.entries com os novos registros. E como utilizamos aquele gist, self.entries vai sempre se manter atualizada com os registros filtrados do banco, mesmo após sua inicialização.
… para sempre manter nossa UI atualizada
tableView.registerNib(R.nib.expenseDisplayTableViewCell) let cellIdentifier = R.nib.expenseDisplayTableViewCell.identifier AppState.current.allEntries.bindTo(tableView.rx_itemsWithCellIdentifier(cellIdentifier, cellType: ExpenseDisplayTableViewCell.self)) {(index, item, cell) in let number = NSNumber(double: item.amount) let value = self.numberFormatter.stringFromNumber(number) ?? "" cell.amountLabel.text = value cell.categoryNameLabel.text = item.category?.description ?? "Sem categoria" cell.applyColor(UIColor.hex(item.category?.tintColor ?? ExpenseCategory.allColors)) }.addDisposableTo(disposeBag)
Aqui estamos criando um datasource dinâmico e anônimo, já configurado para exibir todos os itens de AppState.current.allEntries. Se a lista de itens de allEntries mudar, nossa tableView também será atualizada automaticamente.
E para manter nossos labels atualizados e a tela com a cor da categoria:
AppState.current.currentTintColor.subscribeNext(self.applyColor).addDisposableTo(disposeBag) AppState.current.currentCategory.asObservable().map { (category) -> String in return category?.description ?? "Todas as categorias" }.bindTo(amountType.rx_text).addDisposableTo(disposeBag) AppState.current.currentExpensesTotal.map({ (value) -> String in return self.numberFormatter.stringFromNumber(NSNumber(double: value)) ?? "" }).bindTo(self.amountLabel.rx_text).addDisposableTo(disposeBag)
Com esse código, nossa UI vai estar sempre atualizada com a última categoria selecionada.
Ok, esse RxSwift parece interessante mesmo, mas e a tal da programação funcional? Onde vamos utilizar?
Na verdade, nós já estamos utilizando e talvez você não tenha percebido. Quando executamos:
observableCategory.map { (category) -> Int in guard let selectedCategory = category else { return -1 } return selectedCategory.rawValue }
estamos criando uma função anônima e passando-a como parâmetro para a função map. Porém, poderíamos ter criado uma função assim:
func mapExpenseCategoryToInt(category: ExpenseCategory?) -> Int { guard let selectedCategory = category else { return -1 } return selectedCategory.rawValue }
e utilizado essa função, nossa chamada do map ficaria deste jeito:
observableCategory.map(mapExpenseCategoryToInt)
Como já estávamos fazendo na função que altera a cor do nosso cabeçalho:
AppState.current.currentTintColor.subscribeNext(self.applyColor).addDisposableTo(disposeBag)
Na programação funcional, as funções que criamos são cidadãs de primeira classe, assim como objetos e value types, e podem ser passadas como parâmetros, referenciadas e executadas arbitrariamente.
Hummm, interessante, mas e o Generics? Onde ele se encaixa nessa história toda?
Generics ajuda você a escrever menos código e abranger mais situações. Ao escrever funções genéricas, você está aumentando o escopo onde essas funções podem ser utilizadas. E isso faz todo o sentido quando falamos de RxSwift e programação funcional.
Além do que temos menos código para testar e manter.
Praticamente todos os operadores que utilizamos do RxSwift são genéricos. switchLatest, map, filter, todos podem ser utilizados de maneira genérica, desde que os tipos de retorno desses Observable conformem com os protocolos específicos.
Legal, mas tudo que você mostrou eu consigo fazer sem RxSwift, onde mais ele pode facilitar a minha vida?
Um fato bem interessate da programação reativa é que nós podemos juntar as sequências, criando um fluxo bem complexo a partir de fluxos mais simples e fáceis de testar. E aí a programação reativa começa a brilhar, pois para juntar esses fluxos de maneira imperativa seria necessário muito mais códigos, além de refatoramentos e mudanças nas APIs dos métodos.
Vamos, como exemplo, rastrear o status de conexão com a Internet. Veja esta classe:
import ReachabilitySwift import RxSwift let Reachable = _Reachable() struct _Reachable { let reachability: Reachability let disposeBag = DisposeBag() let internetStatus = Variable<Reachability.NetworkStatus>(.NotReachable) private init() { reachability = try! Reachability.reachabilityForInternetConnection() createObservable().bindTo(self.internetStatus).addDisposableTo(disposeBag) } private func createObservable()-> Observable<Reachability.NetworkStatus> { return Observable.create { observer in let cancel = AnonymousDisposable { self.reachability.stopNotifier() } let reachableUpdateBlock: (Reachability-> Void) = { r in observer.onNext(r.currentReachabilityStatus) } self.reachability.whenReachable = reachableUpdateBlock self.reachability.whenUnreachable = reachableUpdateBlock try! self.reachability.startNotifier() return cancel } } }
Aqui estamos criando uma sequência que informa o estado atual da conexão com a Internet. Se quisermos apenas consultar o último valor enviado, podemos utilizar Reachable.internetStatus.value.
Mas podemos criar funções que operem em cima desse Observable.
func filterInternetIsActive()-> Observable<Reachability.NetworkStatus> { return Reachable.internetStatus.asObservable().filter { status in switch status { case .NotReachable: return false default: return true } } } func filterInternetIsOffline()-> Observable<Reachability.NetworkStatus> { return Reachable.internetStatus.asObservable().filter { status in switch status { case .NotReachable: return true default: return false } } }
Agora, para exibir um alerta sempre que a conexão cair, podemos utilizar filterInternetIsOffline().subscribeNex(funcToShowAlert).
Ou ainda podemos fazer uma requisição ao servidor, e caso estejamos sem Internet, ou ela caia no meio da conexão, podemos tentar novamente quando a conexão voltar:
extension Observable { func retryOnInternetBecomeActive() -> Observable<E> { return self.retryWhen({ (error) -> Observable<Reachability.NetworkStatus> in return filterInternetIsActive() }) } }
Agora com esse método podemos fazer nossa chamada na API e garantir que, caso nossa conexão caia, quando ela voltar esta chamada será refeita:
func tryConnection()-> Observable<NSData> { let request = NSURLRequest(URL: NSURL(string: "http:path_to_api")!) return NSURLSession.sharedSession().rx_data(request).retryOnInternetBecomeActive() }
Depois, basta executar: tryConnection().subscribeNext(funcToParseJSON) e pronto, temos aqui nossa requisição tolerante a quedas de Internet.
Finalizando
Bom, RxSwift tem ainda diversos outros recursos, e infelizmente não vamos conseguir cobrir todos em um único artigo. Mas eu espero que os recursos apresentados aqui, junto com os exemplos fornecidos, possam lhe mostrar como pode ser simples e fácil adicionar programação funcional/reativa no seu código já existente, sem que você tenha que alterar a arquitetura do seu projeto.
E isso é tudo, pessoal! Até o próximo artigo! E acompanhem o desenvolvimento do ExpenseTracker, pois pretendo evoluí-lo com o passar do tempo, adicionando mais recursos funcionais e reativos.
Agradecimentos
- A equipe do RxSwift pela ótima lib que eles entregam
- A equipe do CocoaHeads BR pela iniciativa do EquinociOS! (Valeu Solli!!)