Tags Merge e ViewStub Para Otimizar Sua APP Android

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação. Você receberá um email de confirmação. Somente depois de confirmar é que poderei lhe enviar o conteúdo exclusivo por email.

Email inválido.
Blog /Android /Tags Merge e ViewStub Para Otimizar Sua APP Android

Tags Merge e ViewStub Para Otimizar Sua APP Android

Vinícius Thiengo20/11/2016
(1244) (6) (44) (7)
Go-ahead
"O início de um hábito é como um fio invisível, mas a cada vez que o repetimos o ato reforça o fio, acrescenta-lhe outro filamento, até que se torna um enorme cabo, e nos prende de forma irremediável, no pensamento e ação."
Orison Swett Marden
Código limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas598
Comprar Livro
Conteúdo Exclusivo
Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Opa, blz?

Nesse artigo vamos estudar duas maneiras de otimizar o uso de layouts em APPs Android. Uma delas é mais uma correção do que uma otimização, no caso, a tag <merge>.

Antes que você pense que ambas as entidades apresentadas aqui (<merge> e <ViewStub>) são novas, na verdade não são. Tanto que os dois melhores conteúdos que encontrei dessas tags XML são do blog do Android, mais precisamente estão entre os artigos de Março de 2009.

Note que mesmo essas sendo tags old school, elas são muito úteis até hoje (e muito provavelmente continuarão sendo).

Abaixo seguem os tópicos que estaremos abordando nesse artigo:

O problema de não otimizar o uso de memória

Já faz alguns muitos artigos e vídeos aqui do Blog em que falamos sobre a necessidade de otimizarmos sempre o código de nossas APPs Android com o objetivo de evitarmos vazamento de memória.

Há vários caminhos possíveis para que a memória seja utilizada de forma a aproximar a APP ainda mais da exceção OutOfMemoryException. Na documentação do Android há algumas páginas mostrando aos programadores como evitar essa Exception.

Quando sua APP utiliza imagens, áudios ou vídeos o problema fica ainda mais sério, pois essas mídias consomem muitos dos recursos disponíveis ao aplicativo.

Logo, diferente do que é para softwares Web ou Desktop, qualquer conteúdo sobre otimização de código mobile, mais especificamente sobre código Android, em minha opinião, você deve parar e ler artigo e comentários.

Comentários também?

Sim. Muitas vezes são os comentários que respondem muitas de nossas dúvidas ou até mesmo complementam o conteúdo com dicas valiosas.

Aqui no Blog, sempre que indico algum artigo para programadores com dúvidas, solicito que também leiam os comentários.

Mas enfim... Como o AppJolt nos alertou com a apresentação da API de anúncios dele: quatro de cada cinco aplicativos instalados em aparelhos Android são removidas logo em seguida.

Evitar um OutOfMemoryException aumenta nossas chances de permanecer com nossas APPs nos devices Android.

Seguindo o conteúdo vamos falar sobre técnicas (tags) que trazem ao menos dois benfícios, não somente o de evitar o uso indevido da memória, mas também o que permite que o usuário da APP veja esta carregando e respondendo aos comandos dele de maneira eficiente.

Projeto de exemplo

O projeto utilizado no exemplo do artigo pode ser encontrado na seguinte página no GitHub: https://github.com/viniciusthiengo/merge-viewstub-project

Se estiver acompanhando o Blog a algum tempo notará que o projeto é quase o mesmo do utilizado no artigo da API da In Loco Media. No projeto presente aqui nós removemos essa API de anúncios.

Para melhor aproveitar o conteúdo apresentado no decorrer do post, recomendo que baixe o projeto e o abra em seu Android Studio.

Pois para simplificar e focar nas apresentações das tags <merge> e <ViewStub>, vou colocar somente os códigos Java e XML diretamente relacionados a elas.

Trabalhando com o <merge>

Acessando o projeto no Android Studio e indo ao layout app_bar_main.xml tínhamos a seguinte configuração:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="br.com.thiengo.geolocationads.MainActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_main" />

<FrameLayout
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="#44ffffff">

<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center" />
</FrameLayout>

</android.support.design.widget.CoordinatorLayout>

 

Esse layout é carregado dentro do layout principal da MainActivity via tag <include>. O que nos interessa no código acima, nessa seção, é o layout content_main.xml que está referenciado na tag <include>.

Esse layout, content_main.xml, antes das atualizações no projeto que está no GitHub, tinha o seguinte código XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="br.com.thiengo.geolocationads.MainActivity"
tools:showIn="@layout/app_bar_main"
android:orientation="vertical">

<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>

 

Esse é o código remanescente de quando era inserida, via Java API, uma AdView para anúncios de alguma API de monetização. Agora, sem essa característica, View de anúncios, esse layout está pouco eficiente, pois:

  • O ViewRoot <LinearLayout> não precisa mais ser utilizado, pois os atributos de design nele, que é o que ainda importa para nós, podem ser configurados na tag <ListView>;
  • Os atributos android:layout_height="0"android:layout_weight="1" somente forçam um duplo processamento para a inclusão da View ListView no layout. Esses atributos não mais são necessários, não com esses valores.

Abaixo a figura que representa parte de como está a configuração do layout da MainActivity depois de renderizado: 

Remover o LinearLayout, sem modificar o design atual, é o nosso objetivo.

Uma das opções de melhoria para o layout content_main.xml é a utilização da tag <merge>, pois podemos continuar reaproveitando o ListView, em outros layouts, caso necessário, com as definições de design corretas e não mais estaremos utilizando uma ViewRoot desnecessária que somente ocupa mais espaço na memória reservada a APP aumentando o tempo de renderização do layout.

Abaixo o código atualizado de content_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</merge>

 

O que era necessário reaproveitar da tag <LinearLayout> para manter o mesmo design na tela do usuário nós colocamos na tag <ListView>.

A tag <merge> não deve receber nada além dos atributos de namespace que serão utilizados no layout.

Ok, mas ainda temos uma tag root, <merge>. Qual a real melhoria nesse caso?

Na verdade a tag <merge> não é uma tag root na renderização, pois o Android ignora ela e insere diretamente, no local do include de content_main.xml, as tags filhas dela. Em nosso caso, a tag <ListView> apenas.

Diferente da tag <ScrollView>, a tag <merge> não tem limites quanto ao número de tags filhas. A limitação está mesmo na utilização dela.

Além de não poder ser carregada na tag <ViewStub>, ela também não pode ser utilizada em qualquer outra parte do layout senão como tag root dele.

Abaixo a figura de nossa nova hierarquia de Views, bem, parte da nova hierarquia:

Mesmo que o ganho na velocidade de renderização seja em milissegundos, há o ganho e o uso da memória é melhorado, pois foi removida a View LinearLayout e também o uso do atributo android:layout_weight

Acessando no Android Studio "Tools" > "Android" > "Android Device Monitor", então clicando na aba superior direita "Hierarchy View" e logo em seguida, no lado esquerdo, na aba "Windows" clicando no package de nosso projeto.

Fazendo isso conseguimos visualizar que depois da adição de <merge> ListView se tornou uma tag filha direta de CoordinatorLayout:

Com isso podemos prosseguir para nossa última melhoria, a utilização da tag <ViewStub>

Trabalhando com o <ViewStub>

A ViewStub é muito similar a tag <include>, porém com uma característica que permite melhorar a inicialização do layout que a contém: carregamento de conteúdo somente sob demanda.

Antes da atualização do projeto que está no GitHub, tínhamos o seguinte código no layout app_bar_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="br.com.thiengo.geolocationads.MainActivity">

...
<FrameLayout
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="#44ffffff">

<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center" />
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>

 

Os "..." representam uma série de Views até chegarmos ao FrameLayout. O FrameLayout com o ProgressBar é utilizado para travar a ação de clique do usuário nos itens do ListView quando novos conteúdos estiverem sendo carregados.

Além de passar para o usuário a indicação desse novo carregamento. Ou seja, o FrameLayout somente fica ativo quando a ação do usuário aciona um novo carregamento, a ação de scroll down:

Para Views que não são necessárias no layout logo na inicialização dele, nós podemos utilizar o ViewStub para carregarmos essas Views, layouts, somente quando requisitados.

Dessa forma conseguimos acelerar, para o usuário, a apresentação do layout inicial, pois a utilização do ViewStub no local de um conjunto de Views é muito mais leve.

Nosso objetivo com o ViewStub no projeto de exemplo é chegar a seguinte hierarquia de Views:

Antes de prosseguir com a atualização dos arquivos XML, vamos visualizar o método na MainActivity, loadProgressBar(), que permite a mudança de estado do FrameLayout

...
public void loadProgressBar(boolean status){
int visibility = status ? View.VISIBLE : View.GONE;
findViewById(R.id.progress_bar).setVisibility(visibility);
}
...

 

Aqui não é importante apresentar onde esse método é chamado, pois as invocações a loadProgressBar() serão sempre as mesmas, o conteúdo desse método é que será atualizado.

Vamos começar criando um novo layout, progress_bar.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#44ffffff">

<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center" />
</FrameLayout>

 

Note que da configuração anterior para FrameLayout somente removemos o atributo android:visibility, pois esse não deve mais ser utilizado. Vamos controlar a visibilidade dessa View somente via Java API.

Voltando ao código de app_bar_main.xml, trocamos a tag FrameLayout pela tag ViewStub:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="br.com.thiengo.geolocationads.MainActivity">

...
<ViewStub
android:id="@+id/vs_progress_bar"
android:inflatedId="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/progress_bar" />

</android.support.design.widget.CoordinatorLayout>

 

Os atributos android:layout* já apresentados na tag <ViewStub> acima são todos obrigatórios.

O atributo android:id nos permite um acesso trivial a tag <ViewStub>. O atributo android:inflatedId nos permite sobrescrever o id do layout que será carregado, em nosso caso, o layout progress_bar.xml.

Segue o código do método loadProgressBar(), atualizado:

...
public void loadProgressBar(boolean status){
try{
int visibility = status ? View.VISIBLE : View.GONE;
findViewById(R.id.progress_bar).setVisibility(visibility);
}
catch(Exception e){
((ViewStub) findViewById(R.id.vs_progress_bar)).inflate();
}
}
...

 

A lógica de negócio utilizada foi:

  1. Verifique se é para apresentar ou esconder a View root responsável pelo ProgressBar;
  2. Aplique o novo estado de visualização a View root do ProgressBar;
  3. Caso essa View não exista no layout, acesse a View ViewStub e invoque o método inflate(), logo depois da aplicação do cast.

Note que logo depois de acessarmos o ViewStub e invocarmos o método inflate() ou o método setVisibility(), o ViewStub é removido da hierarquia de Views do layout, a partir desse ponto devemos acessar somente o layout carregado pelo ViewStub.

Observação: O método inflate() retorna o objeto root do layout carregado pelo ViewStub.

Com essa última atualização, podemos rodar a aplicação no emulador e então, antes de realizar o scroll down, acessar o "Hierarchy View", como fizemos na seção anterior, para verificar se nosso objetivo foi alcançado:

Period! Com isso deixamos o carregamento inicial do layout app_bar_main.xml ainda mais eficiente.

Conteúdos complementares

Obviamente que os links em Fontes vão lhe ajudar a se aprofundar no assunto: Otimização de APPs Android. Mas abaixo deixo alguns links, alguns também com vídeo, que vão lhe ajudar a obter ainda mais de seus códigos Java Android:

Vídeo com implementação passo a passo do exemplo

No vídeo a seguir mostro passo a passo a implementação das tags <merge> e <ViewStub>, como no exemplo do artigo:

Como já informado no início do conteúdo, para ter acesso completo ao código do projeto entre no seguinte GitHub: https://github.com/viniciusthiengo/merge-viewstub-project.

Conclusão

As tags <merge> e <ViewStub> são bem simples de utilizar e podem nos dar ganhos consideráveis em termos de melhora de desempenho de APPs Android.

O ponto negativo fica com a necessidade de destrinchar o código a olho nu para saber quando utiliza-las e então otimizar os layouts sempre que possível. O Lint pode ajudar, mas no caso do ViewStub, provavelmente não muito.

Não deixe de utilizar as ferramentas "Hierarchy View" e "Layout Inspector" para ajudar na busca de melhorias e também para certificar que sua nova configuração de layout, com as tags discutidas aqui, está sendo seguida.

Não deixe de utilizar as tags <merge> e <ViewStub> sempre que enxergada a oportunidade, mesmo a otimização não sendo sua prioridade incial.

Ainda há outros conteúdos muito próximos as tags utilizadas aqui e que melhoram ainda mais a performance da APP, um deles é o WeakReference que será abordado em artigos futuros aqui no blog. Então não deixe de se inscrever logo abaixo para receber os novos conteúdos em primeira mão.

Fontes

Avoiding memory leaks

Re-using Layouts with <include/>

Loading Views On Demand

Android Layout Tricks #3: Optimize by merging

Android Layout Tricks #3: Optimize with stubs

Android ListView setOnScrollListener

Vlw.

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Relacionado

ConstraintLayout, Melhor Performance no AndroidConstraintLayout, Melhor Performance no AndroidAndroid
Scrum - A Arte de Fazer o Trabalho na Metade do TempoScrum - A Arte de Fazer o Trabalho na Metade do TempoLivros
Proguard AndroidProguard AndroidAndroid
Lint Tool Para Alta Performance em APPs AndroidLint Tool Para Alta Performance em APPs AndroidAndroid

Compartilhar

Comentários Facebook

Comentários Blog (6)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
01/04/2017
Professor porque ao inserir uma outra atividade da erro nesse projeto?
Pena que não dá para carregar imagem para tu ver!

Error:(39, 0) Could not get unknown property 'compile' for object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.
Responder
Vinícius Thiengo (0) (0)
01/04/2017
Alan, tudo bem?

Se possível, descreva passo a passo o que você está fazendo até o erro ser gerado.

Sobre a imagem / print, você pode coloca-la em um servidor online gratuito de imagens, até mesmo no Google Plus, e assim compartilhar o link dela aqui. Abraço.
Responder
01/04/2017
O erro foi este professor!
https://drive.google.com/file/d/0B6RKmeUX9iJQbExlRUJDVy1NVmM/view?usp=sharing

Descrição até chegar no erro: Apenas fui em File/New/Activty/Empty Activity.
Fui criar uma nova activity para colocar uma webView e o erro apareceu.
Responder
Vinícius Thiengo (0) (0)
01/04/2017
Alan, a princípio o problema está no Gradle, ou o App Level ou o Project Level.

Se possível, coloque-os no PasteBin (https://pastebin.com/) e compartilhe aqui o link do paste, assim vejo o que pode ser.

Coloque o print dos grades também, para ver se há as linhas vermelhas indicando problemas de referência.

Abraço.
Responder
01/04/2017
Professor não dei conta do Patebin não me perdoe, estou compartilhando continuação do seu projeto com erro pelo google drive:

https://drive.google.com/file/d/0B6RKmeUX9iJQR0NlZWdya0tDaVU/view?usp=sharing

Se deu certo me avise por favor!!! Muito obrigado por sua atenção e dispor de seu precioso tempo para compartilhar seus conhecimentos conosco.
Responder
Vinícius Thiengo (0) (0)
09/04/2017
Alan, tudo bem?

Seu problema na verdade é um pequeno bug que veio junto a nova versão do Android Studio.

Os Gradle App Level fica desorganizado algumas vezes. O seu estava assim:

https://drive.google.com/file/d/0B0fyTi4aZKXNY2RRRTdHd3NkbFE/view

Por isso teve problemas. Ele deveria estar assim:

https://drive.google.com/file/d/0B0fyTi4aZKXNQlJHWXBKQnk1M2c/view

Assim ele funciona. Faça essa alteração em seu projeto. Abraço.
Responder