A MegaFon escolheu a oportunidade de trabalhar com comunicações instáveis como um de seus importantes pontos de crescimento. Existem lugares na Rússia onde as comunicações são temporariamente desconectadas ou perdidas por um longo tempo. E é preciso que mesmo nesse caso o aplicativo funcione sem falhas.
Valentin falou sobre como essa tarefa foi realizada nos últimos cinco meses, como a arquitetura do projeto foi escolhida e implementada, quais tecnologias foram usadas, bem como o que elas alcançaram e o que foi planejado para o futuro, Valentin falou na Conferência Apps Live 2020 de desenvolvedores de aplicativos móveis.
Uma tarefa
A empresa disse - ficamos offline para que o usuário possa interagir com sucesso com o aplicativo em uma conexão de rede instável. Nós, como equipe de desenvolvimento, tínhamos que garantir o Offline primeiro - o aplicativo funcionará mesmo com uma Internet instável ou completamente ausente. Hoje contarei a vocês onde começamos e quais os primeiros passos que demos nessa direção.
Pilha de tecnologia
Além da arquitetura MVC padrão, usamos:
Swift + Objective-C
A maior parte do código (80% do nosso projeto) é escrita em Objective-C. E já escrevemos novo código em Swift.
Arquitetura modular
Nós dividimos logicamente os pedaços de código global em módulos para obter uma compilação, lançamento de projeto e desenvolvimento mais rápidos.
Submódulos (bibliotecas)
Conectamos todas as bibliotecas adicionais por meio do submódulo git para obter mais controle sobre as bibliotecas usadas. Portanto, se o suporte para qualquer um deles parar repentinamente, seremos capazes de corrigir a situação por conta própria.
Dados principais para armazenamento local
Ao escolher, o principal critério para nós foi a natalidade e a integração com os frameworks iOS. E essas vantagens do Core Data foram decisivas:
- Salve automaticamente a pilha e os dados que recebemos;
- , ( , ..)
- ;
- ;
- ;
- ;
- UI (FRC);
- (NSPredicates).
UIManaged document
O kit de UI possui uma classe interna chamada UIManagedDocument, que é uma subclasse de UIDocument. Sua principal diferença é que, quando um documento gerenciado é inicializado, uma URL é especificada para a localização do documento no armazenamento local ou remoto. O objeto de documento, então, cria completamente uma pilha de dados principais pronta para uso, que é usada para acessar o armazenamento persistente do documento usando o modelo de objeto (.xcdatamodeld) do pacote do aplicativo principal. É conveniente e faz sentido, embora já vivamos no século 21:
- O UIDocument salva automaticamente o próprio estado atual, em uma frequência específica. Para seções especialmente críticas, podemos acionar manualmente o salvamento.
- . - — , , - , — , , .
- UIDocument .
- Core data .
- iCloud . , .
- .
- O paradigma de aplicativo baseado em documento é usado - representando o modelo de dados como um contêiner para armazenar esses dados. Se observarmos o modelo MVC clássico na documentação da Apple, podemos ver que os dados do Core foram criados precisamente para manipular esse modelo e nos ajudar a trabalhar com dados em um nível mais alto de abstração. No nível do modelo, trabalhamos conectando o UIManagedDocument com toda a pilha criada. E consideramos o próprio documento como um contêiner que armazena dados do Core e todos os dados do cache (de telas, usuários). Além disso, podem ser fotos, vídeos, textos - qualquer informação.
Consideramos nosso aplicativo, seu lançamento, autorização do usuário e todos os seus dados como uma espécie de grande documento (arquivo) que armazena o histórico de nosso usuário:
Processo
Como projetamos a arquitetura
Nosso processo de design ocorre em várias etapas:
- Análise de especificações técnicas.
- Renderizando um diagrama UML. Usamos principalmente três tipos de diagramas UML: diagrama de classes, fluxograma e diagrama de sequência. Essa é a responsabilidade direta dos desenvolvedores seniores, mas os desenvolvedores com menos experiência também podem fazer isso. Isso é até bem-vindo, pois permite que você mergulhe bem na tarefa e aprenda todas as suas sutilezas. Isso ajuda a encontrar eventuais falhas na atribuição técnica, bem como a estruturar todas as informações sobre a tarefa. E tentamos levar em conta a natureza multiplataforma de nosso aplicativo - trabalhamos em estreita colaboração com a equipe do Android, desenhando o mesmo diagrama em duas plataformas e tentando usar os principais padrões de design geralmente aceitos da gangue de quatro.
- Revisão da arquitetura. Como regra, um colega de uma equipe adjacente conduz a revisão e avaliação.
- Implementação e teste no exemplo de um módulo UI.
- Dimensionamento. Se o teste for bem-sucedido, escalamos a arquitetura para todo o aplicativo.
- Reestruturação. Para verificar se perdemos alguma coisa.
Agora, após cinco meses de desenvolvimento deste projeto, posso mostrar todo o nosso processo em três etapas: o que aconteceu, como mudou e o que aconteceu a partir daí.
O que aconteceu
Nosso ponto de partida foi a arquitetura MVC padrão - são camadas interconectadas:
- Camada UI, totalmente programada usando Objective C;
- Aula de apresentação (modelo);
- A camada de serviço onde trabalhamos com a rede.
O indicador de atividade foi localizado no local do diagrama onde o processo de recebimento de dados é sensível à velocidade da Internet - o usuário quer um resultado rápido, mas é forçado a olhar para alguns carregadores, indicadores e outros sinais. Estes foram nossos pontos de crescimento na experiência do usuário:
Período de transição
Durante o período de transição, tivemos que implementar cache para telas. Mas, como o aplicativo é grande e contém muito código Objective C legado, não podemos simplesmente pegar e excluir todos os serviços e modelos inserindo código Swift - devemos levar em conta que, paralelamente ao armazenamento em cache, ainda temos muitas outras tarefas de produto em desenvolvimento.
Encontramos uma maneira fácil de integrar o código atual da forma mais eficiente possível, sem quebrar nada, e de realizar a primeira iteração da maneira mais suave possível. No lado esquerdo do diagrama anterior, removemos completamente tudo relacionado às solicitações de rede - o serviço agora se comunica com o DataSourceFacade por meio da interface. E agora é com essa fachada que funciona o serviço. Ele espera da DataSource os dados que recebeu anteriormente da rede. E no próprio DataSource, a lógica para extrair esses dados está oculta.
No lado direito do diagrama, dividimos a aquisição de dados em comandos - o padrão de Comando visa executar alguns comandos básicos e obter o resultado. No caso do iOS, usamos os herdeiros de NSOperation:
Cada comando que você vê aqui é uma operação que contém uma unidade lógica da ação esperada. Isso é obter dados de um banco de dados (ou rede) e armazenar esses dados nos dados do Core. Por exemplo, o objetivo principal do AcquireCommand não é apenas retornar a fonte de dados para a fachada, mas também nos permitir projetar o código de forma a receber dados através da fachada. Ou seja, a interação com as operações passa por essa fachada.
E a principal tarefa das operações é passar os dados do DataSource para o DataSourceFacade. Obviamente, construímos a lógica de forma a mostrar os dados ao usuário o mais rápido possível. Normalmente, dentro do DataSourceFacade, temos uma fila operacional onde iniciamos nossas NSOperations. Dependendo das condições configuradas, podemos decidir quando mostrar os dados do cache e quando receber da rede. Quando solicitamos pela primeira vez uma fonte de dados na fachada, vamos ao banco de dados de dados do Core, obtemos os dados de lá por meio de FetchCommand (se houver) e os devolvemos imediatamente ao usuário.
Ao mesmo tempo, lançamos uma requisição paralela de dados através da rede, e quando esta requisição é executada, o resultado chega ao banco de dados, é armazenado nele, e então recebemos uma atualização do nosso DataSource. Esta atualização já está incluída na IU. Desta forma minimizamos o tempo de espera pelos dados, e o usuário, ao recebê-los instantaneamente, não percebe a diferença. Ele receberá os dados atualizados assim que o banco de dados receber uma resposta da rede.
Como se tornou
Vamos para um esquema mais lacônico (e chegaremos no final):
Agora, disso temos:
- Camada de interface do usuário,
- a fachada por meio da qual fornecemos nosso DataSource,
- o comando que retorna este DataSource junto com as atualizações.
O que é um DataSource e por que falamos tanto sobre ele
DataSource é um objeto que fornece dados para a camada de apresentação e segue um protocolo predefinido. E o protocolo deve ser ajustado a nossa IU e fornecer dados para nossa IU (não importa para uma tela específica ou para um grupo de telas).
Um DataSource normalmente tem duas responsabilidades principais:
- Fornecimento de dados para exibição na camada UI;
- Notificando a IU da camada sobre alterações de dados e enviando o lote necessário de alterações para a tela quando recebemos uma atualização.
Usamos várias variantes do DataSource aqui, porque temos muito código legado Objective C - ou seja, não podemos colar facilmente nosso Swift DataSource em todos os lugares. Também não usamos coleções em todos os lugares ainda, mas no futuro iremos reescrever o código especificamente para usar as telas CollectionView.
Um exemplo de um dos nossos DataSource:
Este é um DataSource para uma coleção (é chamado CollectionDataSource) e esta é uma classe bastante simples do ponto de vista da interface. Leva uma coleção configurada por um fetchedResultsController e um CellDequeueBlock. Onde CellDequeueBlock é um alias de tipo no qual descrevemos a estratégia para criar células.
Ou seja, criamos o DataSource e o atribuímos à coleção chamando performFetch no fetchedResultsController e, em seguida, toda a mágica é atribuída à interação de nossa classe DataSource, fetchedResultsController e a capacidade do delegado de receber atualizações do banco de dados:
FetchedResultsController é o coração de nosso DataSource. Você encontrará muitas informações sobre como trabalhar com ele na documentação da Apple. Como regra, recebemos todos os dados com sua ajuda - tanto novos dados quanto dados que foram atualizados ou excluídos. Ao mesmo tempo, solicitamos dados da rede simultaneamente. Assim que os dados foram recebidos e armazenados no banco de dados, recebemos uma atualização do DataSource, e a atualização chegou até nós na IU. Ou seja, com uma solicitação recebemos os dados e os exibimos em diferentes lugares - legais, convenientes, nativos!
E onde quer que seja possível usar DataSource pronto com tabelas ou com coleções, nós o fazemos:
Nos lugares onde temos muitas telas e tabelas e coleções não são usadas (e a programação Objective C é usada), avaliamos quais dados precisamos para a tela, e através do protocolo descrevemos nosso DataSource. Depois disso, escrevemos a fachada - como regra, este também é um protocolo público Objective C através do qual solicitamos nosso DataSource. E então há a entrada para o código Swift.
Assim que estivermos prontos para transferir a tela completamente para a implementação Swift, será suficiente remover o wrapper Objective C - e, graças ao DataSource customizado, podemos trabalhar diretamente com o protocolo Swift.
Atualmente, estamos usando três variantes principais de DataSources:
- TableViewDatasource + estratégia de células (estratégia para criação de células);
- CollectionViewDatasource + estratégia de célula (opção com coleções);
- CustomDataSource é uma opção personalizada. Nós o usamos mais agora.
resultados
Após todas as etapas para projetar, implementar e interagir com o código legado, a empresa recebeu as seguintes melhorias:
- A velocidade de entrega de dados ao usuário aumentou significativamente devido ao cache. Este é provavelmente um resultado óbvio e lógico.
- Estamos agora um passo mais perto do primeiro paradigma offline.
- Os processos de uma revisão de plataforma cruzada arquitetônica foram configurados nas equipes de iOS e Android - todos os desenvolvedores envolvidos neste projeto têm informações e trocam experiências facilmente entre as equipes.
- . , , legacy , .
- , — . , , , , , .
O bônus para nós é que entendemos como trabalhar com arquitetura e diagramas pode ser interessante e divertido (e isso torna o desenvolvimento mais fácil). Sim, passamos muito tempo desenhando e alinhando nossas abordagens arquitetônicas, mas quando se tratava de implementação, dimensionamos muito rapidamente em todas as telas.
Nosso caminho para Offline continua primeiro - não precisamos apenas do cache para ficar offline, mas também que o usuário pode operar sem uma conexão de rede, com sincronização adicional com o servidor depois que a Internet aparecer.
Links
- Guia de programação baseado em documento . Este é um documento bastante antigo, a Apple não recomenda mais usá-lo. Mas eu recomendaria procurar pelo menos um desenvolvimento adicional. Há muitas informações úteis aí.
- Document-based WWDC:
- DataSources
Apps Live 2020 .
— Android iOS, . , , .