
No AppsConf 2018 , que aconteceu de 8 a 9 de outubro, fiz uma apresentação sobre a criação de aplicativos Android inteiramente em uma Atividade. Embora o tópico seja bem conhecido, existem muitos preconceitos com relação a essa escolha - a sala lotada e o número de perguntas após o discurso confirmam isso. Para não esperar a gravação do vídeo, decidi fazer um artigo com uma transcrição do discurso.

O que vou dizer
- Por que e por que devo mudar para Atividade Única
- Uma abordagem universal para resolver tarefas que você está acostumado a resolver em várias atividades
- Exemplos de tarefas comerciais padrão
- Gargalos nos quais o código geralmente é sustentado em vez de fazer tudo honestamente
Por que a atividade única é correta?
Ciclo de vida

Todos os desenvolvedores do Android conhecem o esquema de inicialização a frio do aplicativo. Primeiro, onCreate é chamado na classe Application e, em seguida, o ciclo de vida da primeira Activity entra em ação.
Se houver várias atividades em nosso aplicativo (e houver uma maioria desses aplicativos), acontece o seguinte:
App.onCreate() ActivityA.onCreate() ActivityA.onStart() ActivityA.onResume() ActivityA.onPause() ActivityB.onCreate() ActivityB.onStart() ActivityB.onResume() ActivityA.onStop()
Este é o log de ativação da atividadeB abstrata da ActivityA. Uma linha vazia é o momento em que o lançamento de uma nova tela foi chamado. À primeira vista, está tudo bem. Mas se voltarmos para a documentação, ficará claro: para garantir que a tela fique visível para o usuário e que ele possa interagir com ela, isso só será possível depois de chamar onResume
em cada tela:
App.onCreate() ActivityA.onCreate() ActivityA.onStart() ActivityA.onResume() <-------- ActivityA.onPause() ActivityB.onCreate() ActivityB.onStart() ActivityB.onResume() <-------- ActivityA.onStop()
O problema é que esse log não ajuda a entender o ciclo de vida do aplicativo. Quando o usuário ainda está dentro, e quando ele já mudou para outro aplicativo ou minimizou o nosso e assim por diante. E isso é necessário quando queremos vincular a lógica de negócios ao LC do aplicativo, por exemplo, manter uma conexão de soquete enquanto o usuário estiver no aplicativo e fechá-la ao sair
Em um aplicativo de atividade única, tudo é simples - o LC Activity se torna um aplicativo de LC. Tudo o que você precisa para qualquer lógica é fácil de vincular ao estado do aplicativo.
Telas de lançamento
Como usuário, sempre me deparei com o fato de uma chamada da agenda telefônica (e este é claramente o lançamento de uma Atividade separada) não ocorrer depois de clicar em um contato. Não está claro com o que isso está relacionado, mas aqueles a quem tentei passar sem sucesso disseram que receberam a ligação e ouviram o som de passos. Ao mesmo tempo, meu smartphone está no meu bolso há muito tempo.

O problema é que iniciar uma atividade é um processo completamente assíncrono! Não há garantia de inicialização instantânea e, pior ainda, não podemos controlar o processo. Absolutamente.
No aplicativo de atividade única, trabalhando com o gerenciador de fragmentos, podemos controlar o processo.
transaction.commit()
- alterna as telas de forma assíncrona, o que permite abrir ou fechar várias telas ao mesmo tempo.
transaction.commitNow()
- alterna a tela de forma síncrona, se você não precisar adicioná-la à pilha.
fragmentManager.executePendingTransactions () `permite que você execute todas as transações lançadas anteriormente agora.
Análise de pilha de tela
Imagine que a lógica comercial do seu aplicativo depende da profundidade atual da pilha de telas (por exemplo, restrições de aninhamento). Ou, no final de algum processo, você precisa retornar a uma determinada tela e, se houver várias idênticas, à mais próxima da raiz (o início da cadeia).
Como obter uma pilha de atividades? Quais parâmetros devem ser especificados ao iniciar a tela?

A propósito, sobre a mágica das opções de lançamento de atividade:
- você pode especificar sinalizadores de inicialização no Intent (e também misturá-los e alterá-los de lugares diferentes);
- você pode adicionar parâmetros de inicialização no manifesto, porque toda atividade deve ser descrita lá;
- adicione filtros de intenção aqui para lidar com disparos externos;
- e, finalmente, pense nas MultiTasks, quando o Activities for executado em diferentes "tarefas".
Juntos, isso cria confusão e problemas com a depuração de suporte. Você nunca pode dizer com certeza exatamente como a tela foi lançada e como isso afetou a pilha.
Em um aplicativo de atividade única, todas as telas alternam apenas através de transações de fragmento. Você pode analisar a pilha atual de telas e transações salvas.
Na demonstração da biblioteca Cicerone , é possível ver como o status atual da pilha é exibido na barra de ferramentas.

Nota: na versão mais recente, as bibliotecas de suporte bloquearam o acesso à matriz de fragmentos dentro do gerenciador de fragmentos, mas se você realmente quiser, esse problema sempre pode ser resolvido.
Apenas uma atividade na tela
Em aplicativos reais, definitivamente precisaremos combinar telas "lógicas" em uma atividade, para que você não possa escrever um aplicativo real apenas na atividade. A dualidade da abordagem é sempre ruim, pois o mesmo problema pode ser resolvido de maneiras diferentes (em algum lugar, o layout está diretamente na Atividade e, em algum lugar, a Atividade é apenas um contêiner).
Não mantenha atividades
Este sinalizador para teste realmente permite encontrar alguns erros no aplicativo, mas o comportamento que ele reproduz NUNCA ocorre na realidade! Não acontece que o processo de inscrição permaneça e nesse momento a Atividade, embora não ativa, morra! As atividades podem morrer apenas com o processo de inscrição. Se o aplicativo for exibido para o usuário e o sistema não tiver recursos suficientes, tudo ao seu redor morrerá (outros aplicativos inativos, serviços e até o iniciador), e seu aplicativo viverá até o fim, e se precisar morrer, será totalmente.
Você pode conferir.
Legado
Historicamente, há uma enorme quantidade de lógica desnecessária no Activity que provavelmente não é útil para você. Por exemplo, tudo o que você precisa para trabalhar com loaders
, actionBar
, action menu
e assim por diante. Isso torna a classe em si bastante massiva e pesada.
Animações
Talvez qualquer pessoa possa fazer uma animação de mudança simples ao alternar entre a Atividade. Aqui, vale esclarecer que você precisa fazer um desconto na assincronia do lançamento da Atividade, sobre a qual falamos anteriormente.
Se você precisar de algo mais interessante, lembre-se de exemplos de animações de transição feitas na Atividade:

Mas há um grande problema: personalizar essa animação é quase impossível. É improvável que agrade designers e clientes.
Com fragmentos, tudo é diferente. Podemos ir direto ao nível da hierarquia da exibição e fazer qualquer animação que você possa imaginar! Evidência direta aqui :

Se você olhar o código-fonte, verá que isso é feito em um layout regular. Sim, o código é decente lá, mas a animação é sempre difícil o suficiente, e ter essa oportunidade é sempre uma vantagem. Se você tiver duas atividades ativadas, o aplicativo não terá um contêiner comum onde você poderá fazer essas transições.
Configurar a configuração imediata
Este ponto não estava no meu discurso, mas também é muito importante. Se você tiver um recurso para alternar o idioma dentro do aplicativo, com várias Atividades, será bastante problemático implementá-lo, se, entre outras coisas, você não precisar reiniciar o aplicativo, mas permanecer no mesmo local em que o usuário estava no momento da chamada da funcionalidade.
Em um aplicativo de Atividade Única, basta alterar o código do idioma instalado no contexto do aplicativo e chamar recreate()
na Atividade, o restante do sistema fará tudo sozinho.
No final
O Google possui uma solução de navegação, cuja documentação afirma explicitamente que é aconselhável escrever aplicativos de atividade única.
Neste ponto, espero que você não tenha dúvidas de que a abordagem clássica com várias atividades contenha uma série de deficiências, das quais é habitual fechar os olhos, se escondendo atrás da tendência geral do descontentamento do Android.
Se sim, então por que a Atividade Única ainda não é um padrão de desenvolvimento?
Aqui vou citar meu bom amigo:

Ao iniciar um novo projeto sério, qualquer líder tem medo de errar e evita decisões arriscadas. Isto está correto. Mas tentarei fornecer um plano abrangente para a transição para a atividade única.
Mudando para atividade única

Se você estudar esse aplicativo, poderá determinar a partir das animações e comportamentos característicos que ele está escrito em várias atividades. Eu posso estar errado e tudo foi feito mesmo em visualizações personalizadas, mas isso não afetará nosso raciocínio.
Agora atenção! Fazemos assim:

Fizemos apenas duas alterações: adicionamos a classe AppActivity e substituímos toda a atividade pelo FlowFragment. Considere cada alteração com mais detalhes.
O AppActivity é responsável por:
- contém apenas um recipiente para fragmentos
- é o ponto de inicialização dos objetos da interface do usuário do escopo (costumava ser feito no aplicativo, o que está errado, porque, por exemplo, os objetos de serviço em nosso aplicativo definitivamente não precisam desses objetos)
- é um provedor de aplicativos
- traz todos os benefícios da atividade única.
O que é FlowFragment :
- faz exatamente a mesma coisa que a atividade, em vez da qual ela foi criada.
Nova navegação
A principal diferença da abordagem antiga é a navegação.

Anteriormente, o desenvolvedor tinha uma opção: iniciar uma nova atividade ou uma transação de fragmento na atual. A escolha não desapareceu, mas os métodos foram alterados - agora precisamos decidir se devemos iniciar a fragmentação do fragmento no AppActivity ou dentro do FlowFragment atual.

Da mesma forma com o processamento do botão Voltar. Anteriormente, a Atividade passava o evento para o fragmento atual e, se não o processasse, tomava a própria decisão. Agora AppActivity passa o evento para o FlowFragment atual e, por sua vez, passa para o fragmento atual.
Transferindo resultados entre telas
Para desenvolvedores inexperientes, a questão da transferência de dados entre telas é o principal problema da nova abordagem, porque anteriormente era possível usar a funcionalidade startActivityForResult ()!
Não no primeiro ano, várias abordagens arquiteturais para escrever aplicativos foram discutidas. A principal tarefa ao mesmo tempo continua sendo a separação da interface do usuário e a camada de dados e a lógica de negócios. Desse ponto de vista, startActivityForResult () quebra o cânone, pois os dados entre as telas de um aplicativo são transferidos no lado da entidade da camada da interface do usuário. Enfatizo que é apenas um aplicativo, pois temos uma camada de dados comum, modelos comuns em um escopo global e assim por diante. Não usamos essas oportunidades e nos dirigimos à estrutura de um pacote (serialização, tamanho e muito mais).
Meu conselho : não use startActivityForResult () dentro do aplicativo! Use-o apenas para a finalidade a que se destina - para executar aplicativos externos e obter resultados com eles.
Como então iniciar uma tela com uma opção para outra tela? Existem três opções:
- Targetfragment
- Eventbus
- modelo a jato
TargetFragment - uma opção "pronta para uso", mas a mesma transferência de dados no lado da camada da interface do usuário. Opção ruim.
EventBus - se você pode concordar com uma equipe e - o mais importante - controlar os arranjos, pode implementar a transferência de dados entre telas no barramento de dados global. Mas, como essa é uma jogada perigosa, a conclusão é uma má opção.
Modelo reativo - essa abordagem implica a presença de retornos de chamada e muito mais. Como você os implementa é decidido pela equipe de cada projeto. Mas essa abordagem é ideal, pois fornece controle sobre o que está acontecendo e não permite que o código seja usado para outros fins. Nossa escolha!
Sumário
Adoro novas abordagens quando são simples e têm benefícios óbvios. Espero que este seja o caso neste caso. Os benefícios são descritos na primeira parte e você deve julgar a dificuldade. Basta substituir toda a atividade pelo FlowFragment, mantendo toda a lógica inalterada. Mude um pouco o código de navegação e pense em trabalhar com a transferência de dados entre telas, se ainda não o tiver feito.
Para mostrar a simplicidade da abordagem, mudei o aplicativo aberto para Atividade Única e levou apenas algumas horas (é claro, vale a pena considerar que esse não é um legado antigo e que tudo é mais ou menos bom com a arquitetura).
O que aconteceu?
Vamos ver como resolver problemas padrão em uma nova abordagem.
BottomNavigationBar e NavigationDrawer
Usando a regra simples de substituir toda a Atividade pelo FlowFragment, o menu lateral estará agora em algum fragmento e alternará fragmentos aninhados:

Semelhante ao BottomNavigationBar.
É muito mais interessante podermos investir alguns FlowFragment em outros, pois ainda são fragmentos comuns!

Esta opção pode ser encontrada no GitFox .
É a possibilidade de simplesmente combinar alguns fragmentos entre outros que possibilita a criação de uma interface de usuário dinâmica para diferentes dispositivos sem problemas: tablets + smartphones.
Escopos DI
Se você possui um fluxo de compras de produtos em várias telas e precisa mostrar o nome do produto em cada tela, provavelmente já o colocou em uma Atividade separada que armazena o produto e o fornece às telas.
Será o mesmo com o FlowFragment - ele conterá uma escala DI com modelos para todas as telas aninhadas. Essa abordagem elimina o controle complicado da vida útil do escopo, vinculando-o à vida útil do FlowFragment.

Links diretos
Se você usou filtros no manifesto para iniciar uma tela específica no link direto, poderá ter problemas ao iniciar a Atividade, sobre a qual escrevi na primeira parte. Na nova abordagem, todos os links diretos se enquadram em AppActivity.onNewIntent. Além disso, de acordo com os dados obtidos, há uma transição para a tela necessária (ou uma cadeia de telas. Proponho examinar essa funcionalidade no Chicheron ).

Processo de morte
Se o aplicativo estiver gravado em várias Atividades, você deve saber que, quando o aplicativo morrer, e quando o processo for restaurado, o usuário estará na última Atividade, e todos os anteriores serão restaurados somente quando retornarem a eles.

Se você não considerar isso com antecedência, poderão surgir problemas. Por exemplo, se o escopo necessário na última Atividade foi aberto na anterior, ninguém o recriará. O que fazer Trazer isso para a classe Application? Vários pontos abrem escopos?
Tudo é mais simples com fragmentos, pois eles estão dentro de uma Atividade ou outro FlowFragment, e qualquer contêiner será restaurado ANTES de recriar o fragmento.

Podemos discutir outras tarefas práticas nos comentários, caso contrário, há uma chance de o artigo ser muito volumoso.
E agora a parte mais interessante.
Gargalos (você precisa se lembrar e pensar).
Aqui estão reunidas coisas importantes que você deve pensar em qualquer projeto, mas todo mundo está tão acostumado a "rasgá-las" em projetos de várias Atividades que vale a pena lembrar e dizer como resolvê-las corretamente na nova abordagem. E primeiro na lista
Rotação da tela
Esse é o conto mais terrível para os fãs chorões que o Android recria o Activity quando a tela é girada. O método de solução mais popular é corrigir a orientação de retrato. Além disso, essa proposta não é mais feita por desenvolvedores, mas por gerentes assustados com frases como " manter uma curva é muito difícil e custa várias vezes mais ".
Não discutiremos sobre a exatidão de tal decisão. Outra coisa é importante: fixar a rotação não isenta da morte da Atividade! Como os mesmos processos ocorrem com muitos outros eventos: modo dividido, quando vários aplicativos são exibidos na tela, conectando um monitor externo, alterando a configuração do aplicativo rapidamente e assim por diante.
Além disso, a rotação da tela permite verificar a "elasticidade" correta do layout, portanto, em nossa equipe de São Petersburgo, não desativamos a rotação em todas as montagens de vendas, mesmo que não esteja na versão de lançamento. Sem mencionar os erros típicos que ainda serão encontrados durante a verificação.
Muitas soluções já foram escritas para virar, começando no Moxy e terminando com várias implementações do MVVM. Faça com que não seja mais difícil do que qualquer outra coisa.
Considere outro caso interessante.
Imagine um aplicativo de catálogo de produtos. Fazemos isso em atividade única. Em todos os lugares o modo retrato é fixo, mas o cliente deseja um recurso quando, ao visualizar a galeria de fotos, o usuário pode assisti-los na orientação paisagem. Como apoiar isso?
Alguém oferecerá a primeira muleta :
<activity android:name=".AppActivity" android:configChanges="orientation" />
override fun onConfigurationChanged(newConfig: Configuration?) { if (newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Portanto, não podemos chamar super.onConfigurationChanged(newConfig)
, mas processá-lo e rotacionar apenas a visualização necessária na tela.
Mas com a API 23, o projeto trava com uma SuperNotCalledException
, uma péssima escolha .
As declarações acima cometeram um erro:
Fui razoavelmente corrigido nos comentários de que a adição de android: configChanges = "guidance | screenSize" é suficiente e, em seguida, você pode chamar super e Activity não será recriado após a rotação. É útil usá-lo quando um WebView ou mapa está na tela que leva muito tempo para inicializar e você deseja evitar isso.
Isso ajudará a resolver o caso descrito com a galeria, mas a principal mensagem desta seção: não ignore a recriação de Activity , isso pode acontecer em muitos outros casos.
Alguém pode sugerir outra solução:
<activity android:name=".AppActivity" android:screenOrientation="portrait" /> <activity android:name=".RotateActivity" />
Mas, dessa maneira, nos afastamos da abordagem de atividade única para resolver um problema simples e nos privamos de todos os benefícios da abordagem. Esta é uma muleta, e uma muleta é sempre uma má escolha .
Aqui está a solução certa:
<activity android:name=".AppActivity" android:configChanges="orientation" />
override fun onResume() { super.onResume() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR } override fun onPause() { super.onPause() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT }
Ou seja, quando o fragmento é aberto, o aplicativo começa a "girar" e, quando retorna, é corrigido novamente. Na minha experiência, é assim que o aplicativo AirBnB funciona . Se você abrir uma vista das fotos da habitação, o processamento de turnos será ativado, mas na orientação paisagem, você pode arrastar a foto para baixo para sair da galeria. Abaixo dela, a tela anterior ficará visível na orientação paisagem, que você normalmente não encontrará, pois imediatamente após sair da galeria, a tela se tornará um retrato e será corrigida.

É aqui que a preparação oportuna para as rotações da tela ajudará.
Barra de status transparente
Somente Activity pode funcionar com a barra do sistema, mas agora temos apenas uma, portanto, você deve sempre especificar
<item name="android:windowTranslucentStatus">true</item>
Mas, em algumas telas, não é necessário "rastrear" por baixo e é necessário exibir todo o conteúdo abaixo. A bandeira vem em socorro
android:fitsSystemWindows="true"
que indica o layout que você não deve desenhar sob a barra do sistema. Mas se você especificá-lo no layout do fragmento e tentar exibi-lo através da transação no gerenciador de fragmentos, ficará desapontado ... não funcionará!
A resposta é rapidamente google
Eu recomendo que você se familiarize com uma resposta realmente abrangente e com muitos links úteis.
Uma solução rápida e funcional ( mas não a correta ) é agrupar o layout no CoordinatorLayout
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> </android.support.design.widget.CoordinatorLayout>
Uma solução melhor ajuda a processar o teclado também.
Alterar o layout quando o teclado aparecer
Quando o teclado sair, o layout deve mudar para que elementos importantes da interface do usuário não fiquem fora de alcance. E, se anteriormente, poderíamos especificar diferentes modos de reação para o teclado para diferentes atividades, agora precisamos fazer isso na atividade única. Portanto, é necessário usar
android:windowSoftInputMode="adjustResize"
Se você usar a abordagem da seção anterior para processar uma barra de status transparente, encontrará um erro infeliz: se um fragmento rastrear com êxito sob a barra de status, quando o teclado aparecer, ele encolherá acima e abaixo, conforme a barra de status e o teclado dentro do sistema funcionem SystemWindows
.
Preste atenção ao título

O que fazer Leia a documentação!E não deixe de ver Chris Banes falar sobre WindowInsets .
Usar WindowInsets permitirá
- descubra a altura correta da barra de status (e não o código 51dp)
- prepare o aplicativo para qualquer recorte nas telas dos novos smartphones
- descubra a altura do teclado (é real!)
- Receba eventos e responda à aparência do teclado.
Todo mundo aprende WindowInsets!
Tela inicial
Se outra pessoa não souber, a tela inicial canônica não é a primeira tela no aplicativo que carrega dados, mas o que o usuário vê na inicialização até que o conteúdo da atividade tenha tempo para renderizar. Existem muitos artigos sobre esse assunto.

, Single-Activity, Splash screen. , deep-link Splash screen .
, , , .
, . Single-Activity. - , , .
...
Intent, , ...
O que vem a seguir? :
, . ? — «» «» .
O que fazer , .
Activity!
, : , — .
— , ( Activity), .
Activity — . Activity, . .

Conclusão
() , Activity, Android-. , .
: Google . — , , Activity .
, , , ! Obrigada