Guia do desenvolvedor Android para padrão de navegação de fragmentos
Publicados: 2022-03-11Ao longo dos anos, vi muitas implementações de padrões de navegação diferentes no Android. Alguns dos aplicativos estavam usando apenas atividades, enquanto outros atividades misturadas com fragmentos e/ou com visualizações personalizadas.
Uma das minhas implementações de padrão de navegação favoritas é baseada na filosofia “One-Activity-Multiple-Fragments”, ou simplesmente o Fragment Navigation Pattern, onde cada tela no aplicativo é um Fragment de tela cheia e todos ou a maioria desses fragmentos estão contidos em uma Atividade.
Essa abordagem não apenas simplifica a forma como a navegação é implementada, mas tem um desempenho muito melhor e, consequentemente, oferece uma melhor experiência ao usuário.
Neste artigo, veremos algumas implementações de padrões de navegação comuns no Android e, em seguida, apresentaremos o padrão de navegação baseado em Fragment, comparando e contrastando com os outros. Um aplicativo de demonstração que implementa esse padrão foi carregado no GitHub.
Mundo das Atividades
Um aplicativo Android típico que usa apenas atividades é organizado em uma estrutura semelhante a uma árvore (mais precisamente em um gráfico direcionado) onde a atividade raiz é iniciada pelo iniciador. À medida que você navega no aplicativo, há uma pilha de retorno de atividade mantida pelo sistema operacional.
Um exemplo simples é mostrado no diagrama abaixo:
A atividade A1 é o ponto de entrada em nosso aplicativo (por exemplo, representa uma tela inicial ou um menu principal) e a partir dela o usuário pode navegar para A2 ou A3. Quando você precisa se comunicar entre atividades, você pode usar o startActivityForResult() ou talvez compartilhar um objeto de lógica de negócios globalmente acessível entre elas.
Quando você precisa adicionar uma nova atividade, você precisa executar as seguintes etapas:
- Defina a nova atividade
- Registre-o no AndroidManifest.xml
- Abra-o com um startActivity() de outra atividade
Claro que este diagrama de navegação é uma abordagem bastante simplista. Pode se tornar muito complexo quando você precisa manipular a pilha de retorno ou quando você precisa reutilizar a mesma atividade várias vezes, por exemplo, quando você gostaria de navegar pelo usuário por algumas telas de tutorial, mas cada tela de fato usa a mesma atividade como um base.
Felizmente, temos ferramentas para isso chamadas de tarefas e algumas diretrizes para navegação adequada na pilha de retorno.
Então, com a API de nível 11 vieram os fragmentos…
mundo dos fragmentos
O Android introduziu fragmentos no Android 3.0 (API de nível 11), principalmente para oferecer suporte a designs de interface do usuário mais dinâmicos e flexíveis em telas grandes, como tablets. Como a tela de um tablet é muito maior que a de um aparelho, há mais espaço para combinar e trocar componentes de interface do usuário. Os fragmentos permitem esses designs sem a necessidade de gerenciar alterações complexas na hierarquia de visualizações. Ao dividir o layout de uma atividade em fragmentos, você pode modificar a aparência da atividade em tempo de execução e preservar essas alterações em uma pilha de retorno gerenciada pela atividade. – citado no guia de API do Google para Fragments.
Esse novo brinquedo permitiu que os desenvolvedores construíssem uma interface do usuário com vários painéis e reutilizassem os componentes em outras atividades. Alguns desenvolvedores adoram isso, enquanto outros não. É um debate popular usar fragmentos ou não, mas acho que todos concordam que fragmentos trazem complexidade adicional e os desenvolvedores realmente precisam entendê-los para usá-los corretamente.
Pesadelo de fragmentos em tela cheia no Android
Comecei a ver cada vez mais exemplos em que os fragmentos não representavam apenas uma parte da tela, mas na verdade a tela inteira era um fragmento contido em uma atividade. Uma vez eu até vi um design onde cada atividade tinha exatamente um fragmento de tela cheia e nada mais e a única razão pela qual essas atividades existiam era hospedar esses fragmentos. Além da óbvia falha de projeto, há outro problema com essa abordagem. Dê uma olhada no diagrama abaixo:
Como A1 pode se comunicar com F1? O poço A1 tem controle total sobre a F1 desde que criou a F1. A1 pode passar um pacote, por exemplo, na criação de F1 ou pode invocar seus métodos públicos. Como F1 pode se comunicar com A1? Bem, isso é mais complicado, mas pode ser resolvido com um padrão de retorno de chamada/observador onde o A1 se inscreve em F1 e F1 notifica A1.
Mas como A1 e A2 podem se comunicar? Isso já foi abordado, por exemplo, via startActivityForResult() .
E agora vem a verdadeira questão: como F1 e F2 podem se comunicar? Mesmo neste caso, podemos ter um componente de lógica de negócios que está disponível globalmente, para que possa ser usado para passar dados. Mas isso nem sempre leva a um design elegante. E se F2 precisar passar alguns dados para F1 de forma mais direta? Bem, com um padrão de callback F2 pode notificar A2, então A2 termina com um resultado e esse resultado é capturado por A1 que notifica F1.
Essa abordagem precisa de muito código clichê e rapidamente se torna uma fonte de bugs, dor e raiva.
E se pudéssemos nos livrar de todas as atividades e manter apenas uma delas que guarda o resto dos fragmentos?
Padrão de navegação de fragmentos
Ao longo dos anos, comecei a usar o padrão “One-Activity-Multiple-Fragments” na maioria dos meus aplicativos e ainda o uso. Há muitas discussões por aí sobre essa abordagem, por exemplo aqui e aqui. O que eu perdi, no entanto, é um exemplo concreto que eu posso ver e testar.
Vejamos o seguinte diagrama:
Agora temos apenas uma atividade de contêiner e temos vários fragmentos que têm novamente uma estrutura semelhante a uma árvore. A navegação entre eles é feita pelo FragmentManager , ele tem seu back stack.
Observe que agora não temos o startActivityForResult() , mas podemos implementar um padrão de retorno de chamada/observador. Vamos ver alguns prós e contras dessa abordagem:
Prós:
1. AndroidManifest.xml mais limpo e mais fácil de manter
Agora que temos apenas uma Activity, não precisamos mais atualizar o manifesto toda vez que adicionamos uma nova tela. Ao contrário das atividades, não precisamos declarar fragmentos.
Isso pode parecer uma coisa pequena, mas para aplicativos maiores que têm mais de 50 atividades, isso pode melhorar significativamente a legibilidade do arquivo AndroidManifest.xml .
Veja o arquivo de manifesto do aplicativo de exemplo que possui várias telas. O arquivo de manifesto ainda permanece super simples.

<?xml version="1.0" encoding="utf-8"?> package="com.exarlabs.android.fragmentnavigationdemo.ui" > <application android:name= ".FragmentNavigationDemoApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.exarlabs.android.fragmentnavigationdemo.ui.MainActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2. Gerenciamento de navegação centralizado
No meu exemplo de código, você verá que eu uso o NavigationManager que no meu caso é injetado em cada fragmento. Esse gerenciador pode ser usado como um local centralizado para registro, gerenciamento de backstack e assim por diante, para que os comportamentos de navegação sejam desacoplados do restante da lógica de negócios e não espalhados em implementações de telas diferentes.
Vamos imaginar uma situação em que gostaríamos de iniciar uma tela onde o usuário pode selecionar alguns itens de uma lista de pessoas. Você também gostaria de passar alguns argumentos de filtragem como idade, ocupação e gênero.
No caso de Atividades, você escreveria:
Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);
Então você tem que definir o onActivityResult em algum lugar abaixo e lidar com o resultado.
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
Meu problema pessoal com essa abordagem é que esses argumentos são “extras” e não são obrigatórios, então tenho que me certificar de que a atividade de recebimento lide com todos os diferentes casos em que um extra está faltando. Mais tarde, quando alguma refatoração é feita e o extra “age” por exemplo não é mais necessário, então eu tenho que pesquisar em todos os lugares do código onde eu começo esta atividade e me certificar de que todos os extras estão corretos.
Além disso, não seria melhor se o resultado (lista de pessoas) chegasse na forma de _List
No caso de navegação baseada em fragmentos, tudo é mais simples. Tudo o que você precisa fazer é escrever um método no NavigationManager chamado startPersonSelectorFragment() com os argumentos necessários e com uma implementação de retorno de chamada.
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", new PersonSelectorFragment.OnPersonSelectedListener() { @Override public boolean onPersonsSelected(List<Person> selection) { [do something] return false; } });
Ou com RetroLambda
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. Melhores meios de comunicação entre telas
Entre as atividades, podemos compartilhar apenas um Bundle que pode conter primitivos ou dados serializados. Agora, com fragmentos, podemos implementar um padrão de retorno de chamada onde, por exemplo, F1 pode ouvir F2 passando objetos arbitrários. Por favor, dê uma olhada na implementação de retorno de chamada dos exemplos anteriores, que retorna um _List
4. Os Fragmentos de Construção são mais baratos do que as Atividades de Construção
Isso se torna óbvio quando você usa uma gaveta que tem, por exemplo, 5 itens de menu e em cada página a gaveta deve ser exibida novamente.
No caso de navegação de atividade pura, cada página deve inflar e inicializar a gaveta, o que obviamente é caro.
No diagrama abaixo você pode ver vários fragmentos raiz (FR*) que são os fragmentos de tela cheia que podem ser acessados diretamente da gaveta, e também a gaveta só é acessível quando esses fragmentos são exibidos. Tudo o que está à direita da linha tracejada no diagrama está lá como exemplo de um esquema de navegação arbitrário.
Como a atividade do contêiner contém a gaveta, temos apenas uma instância de gaveta, portanto, a cada etapa de navegação em que a gaveta deve estar visível, você não precisa inflar e inicializá-la novamente. Ainda não está convencido de como tudo isso funciona? Dê uma olhada no meu aplicativo de exemplo que demonstra o uso da gaveta.
Contras
Meu maior medo sempre foi que, se eu usasse um padrão de navegação baseado em fragmentos em um projeto, em algum lugar no caminho eu encontraria um problema imprevisto que seria difícil de resolver em torno da complexidade adicional de fragmentos, bibliotecas de terceiros e as diferentes versões do sistema operacional. E se eu tivesse que refatorar tudo o que fiz até agora?
De fato, tive que resolver problemas com fragmentos aninhados, bibliotecas de terceiros que também usam fragmentos como ShinobiControls, ViewPagers e FragmentStatePagerAdapters.
Devo admitir que ganhar experiência suficiente com fragmentos para poder resolver esses problemas foi um processo bastante longo. Mas em todos os casos a questão não era que a filosofia fosse ruim, mas que eu não entendia os fragmentos bem o suficiente. Talvez, se você entendesse os fragmentos melhor do que eu, nem mesmo encontrasse esses problemas.
O único contra que posso mencionar agora é que ainda podemos encontrar problemas que não seriam triviais de resolver, pois não há biblioteca madura por aí que mostre todos os cenários complexos de um aplicativo complexo com navegação baseada em fragmentos.
Conclusão
Neste artigo, vimos uma forma alternativa de implementar a navegação em um aplicativo Android. Comparamos com a filosofia de navegação tradicional que usa atividades e vimos algumas boas razões pelas quais é vantajoso usá-lo em relação à abordagem tradicional.
Caso ainda não o tenha feito, confira o aplicativo de demonstração carregado para a implementação do GitHub. Sinta-se à vontade para fazer um fork ou contribuir com exemplos melhores que mostrariam melhor seu uso.