Este artigo apresenta ao leitor o uso, vantagens e desvantagens da API de Fragmentos do Android. É importante saber que desenvolver para smartphones já não é o bastante hoje em dia. Criar aplicações que possam ser utilizadas em tablets e até mesmo em TVs é um desafio principalmente pelos diferentes tamanhos de tela. A API de Fragmentos se encaixa como uma luva para essas situações, nas quais existem dispositivos com telas de tamanhos variados, pois permite ajustes na interface gráfica possibilitando um melhor aproveitamento dos espaços disponíveis.
O aumento da procura por dispositivos portáteis, como smartphones e tablets, que auxiliam os usuários em suas atividades do cotidiano, tem motivado a indústria na produção de aparelhos com vários tamanhos de tela para diferentes finalidades. Esse fato despertou o interesse dos desenvolvedores a criar soluções que aproveitem ao máximo o espaço de tela disponível nesses aparelhos.
Guia do artigo:
- API de Fragmentos
- Ciclo de vida de um fragmento
- Biblioteca de compatibilidade
- Fragmentos da aplicação
- FragmentRSSList
- FragmentDetail
- Activities da aplicação
- A classe MainActivity
- A classe DetailActivity
- Criando fragmentos dinamicamente
- Fragment Back Stack
Entretanto, tem sido um enorme desafio para os programadores desenvolver aplicativos que se adaptem aos mais variados tamanhos de tela, pois um layout portável com uma boa interface visual e que ocupe todo o espaço da tela, tanto em smartphones quanto em tablets, exige um grande esforço.
Visando reduzir esse esforço foi criada a API de Fragmentos, que tem como objetivos principais a construção de layouts dinâmicos e o reuso de componentes. Os fragmentos são descritos como uma espécie de mini activities, que por sua vez podem ser adicionadas a uma Activity. Dessa forma é possível compor uma tela de diferentes maneiras, agrupando um ou vários fragmentos dentro de uma Activity.
A API de Fragmentos foi introduzida na plataforma Android a partir da versão 3.x (Honeycomb). Apesar disso, está disponível uma biblioteca de compatibilidade (android-support-v4) que permite que alguns recursos presentes no Honeycomb – entre eles, fragmentos – sejam suportados nas versões do Android 2.x para smartphones.
Neste contexto, o intuito deste artigo é apresentar as principais funcionalidades dessa API por meio de exemplos práticos, onde serão expostas suas vantagens e desvantagens. Além disso, também será apresentado ao leitor o ciclo de vida de um fragmento, o uso da biblioteca de compatibilidade e como manipular fragmentos dinamicamente. Ao final, será desenvolvida uma aplicação de leitura de notícias via RSS que utiliza os principais conceitos de fragmentos.
A API de Fragmentos
Um fragmento é um componente reutilizável que auxilia os programadores na criação de layouts para dispositivos com tamanhos de tela variados. Ele representa uma parte da interface gráfica em uma Activity que tem seu próprio ciclo de vida, recebe seus próprios eventos de entrada e pode ser adicionado ou removido enquanto a Activity está em execução.
Para utilizar um fragmento é preciso criar uma classe que estenda android.app.Fragment ou uma de suas subclasses, como ListFragment, DialogFragment ou PreferenceFragment. A Tabela 1 descreve essas classes.
Com o intuito de demonstrar o uso de fragmentos na prática, criaremos uma aplicação de teste que exibe uma mensagem utilizando fragmentos e outra mensagem sem usar esse recurso. Para isso, codificamos a classe MeuFrament, que estende de Fragment. Veja o código desta classe na Listagem 1.
package labs.org.br;
import org.cesar.br.R;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class MeuFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate
(R.layout.exemplo_fragmento, container, false);
}
}
Conforme indicado na Listagem 1, devemos reimplementar o método onCreateView(). Este método é responsável por carregar o layout que será exibido na tela. Ele recebe como parâmetro, entre outros, um objeto do tipo LaytoutInflater, que transforma um layout definido em XML em uma classe View, procedimento feito pelo método inflate(), que possui três argumentos:
- O identificador do layout que será usado;
- Um objeto do tipo ViewGroup, que será o pai da View que está sendo gerada;
- Um parâmetro booleano que, se true, indica que a View carregada deve ser associada ao ViewGroup.
Como pode ser observado, um dos parâmetros do método inflate() é o identificador do layout que será carregado pelo fragmento. Esse identificador aponta para um arquivo XML onde o layout é especificado. A Listagem 2 apresenta um exemplo de como esse arquivo é definido.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fragmento_str" />
</LinearLayout>
Como podemos observar, a Listagem 2 possui um layout que exibe um texto na tela do dispositivo quando a aplicação é carregada.
Até agora criamos um fragmento e definimos o seu layout. O próximo passo consiste em adicioná-lo à activity. Para isso, devemos declarar o fragmento no arquivo de layout da activity, como ilustra a Listagem 3.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/activity_str" />
<fragment android:name="labs.org.br.ExemploFragmento"
android:id="@+id/fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
A tag <fragment> é usada para declarar um fragmento dentro de um arquivo de layout. Ela possui os atributos name, id e tag, descritos na Tabela 2.
No exemplo demonstrado na Listagem 3, percebemos que por meio do atributo name instanciamos a classe de fragmento da Listagem 1 e exibimos o seu conteúdo junto com o layout da Activity. Vale lembrar que cada fragmento possui um identificador único definido pelo atributo id ou tag.
Podemos empregar qualquer um dos dois para obter a instância do fragmento que está em uso, sendo que o último recebe como parâmetro um texto e o primeiro um número. Assim, usa-se o método getFragmentManager() para obter o objeto da classe FragmentManager e, por meio dos seus métodos findFragmentById() e findFragmentByTag(), recuperar a instância do fragmento que se pretende encontrar. A Listagem 4 mostra como podemos acessar MeuFragment.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
MeuFragment meuFragment = (MeuFragment)
getFragmentManager().findFragmentById(R.id.fragment);
}
A linha 6 da Listagem 4 recupera a instância do fragmento por meio do método findFragmentById(), que recebe como parâmetro o identificador do fragmento procurado. No exemplo, o identificador do fragmento foi definido pelo atributo id na Listagem 3.
Depois de elaborar o layout da activity contendo o fragmento, precisamos adicioná-lo à activity por meio do método setContentView(). Isso pode ser feito criando uma classe que estende de Activity e sobrescrevendo o método onCreate(), como pode ser observado na Listagem 5.
package labs.org.br;
import org.cesar.br.R;
import android.app.Activity;
import android.os.Bundle;
public class MinhaActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
É no layout passado por parâmetro para o método setContentView() que definimos por meio da tag <fragment> qual fragmento será utilizado pela nossa Activity. Com isso finalizamos o nosso primeiro exemplo. Ao executá-lo, o resultado deve ser semelhante ao apresentado na Figura 1.
Ciclo de vida de um fragmento
Uma aplicação Android geralmente possui um conjunto de activities ligadas entre si. Elas podem chamar umas às outras a fim de executar ações diferentes, tais como exibir uma listagem, abrir uma tela de cadastro, um tocador de músicas favoritas, acessar um site, etc. Desde o momento em que uma activity é exibida até ser interrompida, ela pode assumir basicamente os estados executando, pausado ou parado.
Para controlar esses estados, assim como o seu ciclo de vida, é necessário o uso dos métodos: onCreate(), onStart(), onRestart(), onPause(), onStop() e onDestroy() da classe Activity.
Um fragmento também possui um ciclo de vida, sendo composto pelos mesmos métodos da activity, mais os métodos onAttach(), onCreateView(), onActvityCreated(), OnDestroyView() e onDetach(). Cabe ressaltar que este artigo assume que o leitor já está habituado com os métodos do ciclo de vida de uma activity, e foca apenas nos métodos exclusivos do ciclo de vida dos fragmentos.
O ciclo de vida do fragmento está intimamente relacionado ao da activity que o declarou, também chamada de host-activity. Desta forma, a chamada a um método do ciclo de vida da activity faz com que o mesmo método também seja invocado em todos os fragmentos dela. Por exemplo, quando o método onResume() de uma activity for chamado, o método onResume() de cada fragmento presente nesta activity também será.
A Figura 2 mostra o fluxo do ciclo de vida de um fragmento.
Como podem ser observados, todos os métodos do ciclo de vida de uma activity também estão presentes no ciclo de vida de um fragmento. Entretanto, como já destacado, existem alguns métodos que são exclusivos a fragmentos, a saber:
- onAttach(activity): Método chamado assim que o fragmento é associado à activity. A activity que possui o fragmento é passada como parâmetro;
- View onCreateView(inflater, viewgroup, bundle): Método chamado no momento de carregar o layout do fragmento, devolve uma instância da classe View contendo o devido layout. A activity utiliza o retorno deste método para colocá-lo no espaço reservado para o fragmento;
- onActivityCreated(): Chamado após o método onCreate() da activity ter sido finalizado;
- onDestroyView(): Chamado quando a view associada ao fragmento está sendo removida;
- onDetach(): Chamado quando o fragmento está sendo desvinculado da activity.
Biblioteca de compatibilidade
A API de Fragmentos do Android auxilia bastante os desenvolvedores na organização do código de uma Activity, mas o uso dela é possível apenas nas versões do Android 3.x ou superiores.
Com o objetivo de possibilitar o uso dos recursos de fragmentos na versão 2.x do Android, criou-se uma biblioteca de compatibilidade, um arquivo JAR que precisa ser adicionado à aplicação que está sendo construída. Para instalar esta biblioteca na aplicação, é necessário fazer o download do arquivo android-support-v4.jar, o que pode ser feito por meio do Android SDK Manager. Veja a Figura 3.
O Android SDK Manager possui um assistente de instalação que irá auxiliá-lo durante o processo de download. Após a instalação ser concluída, é necessário adicionar o JAR ao seu projeto. Isto é feito da seguinte forma:
- Crie um diretório chamado lib na raiz do projeto;
- Copie o arquivo android-support-v4.jar, localizado em /extras/android/support/v4/, e cole na pasta lib;
- Adicione o arquivo copiado ao build path do seu projeto.
Para criar aplicações em versões do Android que não possuem suporte nativo de fragmentos, se faz necessário o uso da biblioteca de compatibilidade. Desta forma, usaremos as classes disponíveis no pacote android.support.v4.
Para isso, duas alterações devem ser feitas no código fonte:
- Estender a classe android.support.v4.FragmentActivity ao invés de Activity;
- Utilizar o método getSupportFragmentManager() ao invés de getFragmentManager(). O método getSupportFragmentManager() retorna uma instância da classe android-support.v4.app.FragmentManager, responsável por gerenciar os fragmentos que estão sendo utilizados na aplicação.
Leitor de notícias via RSS
RSS (Really Simple Syndication) é uma tecnologia baseada em XML que permite a distribuição em tempo real do conteúdo de um website para milhares de outros em todo o mundo. Dessa forma é possível, entre outras coisas, agregar informações de diversos sites sem a necessidade de visitar um a um, realizar o download delas para o usuário acessá-las mesmo se não estiver conectado a internet e exibi-las em aplicações de diversas plataformas (web, mobile, desktop, etc).
Na prática, os websites fornecem suas notícias ou novidades em arquivos no formato XML, conhecidos como feeds. Neles, geralmente são encontradas informações como título, resumo, data e hora e um link para esses conteúdos.
Visando apresentar os principais conceitos da API de Fragmentos, construiremos uma aplicação para leitura de notícias. Nela listaremos os títulos das notícias de um RSS e possibilitaremos ao usuário visualizar os detalhes da mesma após selecioná-la, abrindo-a em outra tela. A Figura 4 mostra como ficará a aplicação em um smartphone.
No smartphone, devido a uma área de visualização mais limitada, a lista de notícias é exibida em uma activity e o conteúdo delas é exibido outra. Já na versão para tablets, como não temos o problema relacionado ao tamanho da tela, tanto a lista de notícias quanto o conteúdo de cada uma são mostrados na mesma activity, como pode ser conferido na Figura 5.
Para criar uma aplicação visando o reuso de componentes, separação de responsabilidades e baixo acoplamento, construiremos dois fragmentos: um que irá listar as notícias do RSS e outro que exibirá os detalhes da notícia selecionada. Além disso, é importante frisar que os fragmentos serão independentes e se comunicarão via parâmetros, o que nos permite definir comportamentos específicos para smartphones e tablets.
Para iniciarmos a construção do leitor de RSS, como de costume, é preciso criar o projeto. Nele vamos utilizar a API de compatibilidade de fragmentos. No Eclipse Juno, a biblioteca de compatibilidade já é incorporada automaticamente. No entanto, caso você utilize outras versões e isso não aconteça, deve-se adicionar o JAR ao build path do projeto, conforme explicado no tópico Biblioteca de Compatibilidade.
Em seguida, é necessária a criação de uma classe que busque as informações no feed e nos retorne uma lista de itens de RSS para que sejam exibidos na tela. Para isso, implementamos uma classe chamada RSSItem, que representa uma notícia do RSS consultado. Ela contém o título e a URL que, quando selecionada, exibirá seu conteúdo em um WebView. A Listagem 6 mostra o código dessa classe.
package br.com.jm.fragments.rss;
import java.io.Serializable;
public class RSSItem implements Serializable {
public static final String RSS_ITEM_PARAM = "RSS_ITEM_PARAM";
private String title;
private String url;
public RSSItem(String title, String url){
this.title = title;
this.url = url;
}
public String getTitle() {
return title;
}
public String getUrl() {
return url;
}
@Override
public String toString() {
return getTitle();
}
}
Após isso, criaremos a classe RSS. Basicamente, ela implementa uma thread para buscar os dados no feed, tendo em vista que essa consulta pode demorar bastante, e é capaz de retornar esses dados em um ArrayList de itens de RSS representados por instâncias de RSSItem. Veja o código dessa classe na Listagem 7.
package br.com.jm.fragments.rss;
import java.io.IOException;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import android.os.AsyncTask;
public class RSS {
public interface ResultListener {
public void onResult(ArrayList<RSSItem> list);
}
public void getNews(String url, ResultListener resultListener){
AsyncTaskRSS asyncTaskRSS = new RSS.AsyncTaskRSS();
asyncTaskRSS.execute(url, resultListener);
}
class AsyncTaskRSS extends AsyncTask<Object,
Void, ArrayList<RSSItem>>{
ResultListener resultListener;
@Override
protected ArrayList<RSSItem> doInBackground
(Object... params) {
String url = (String) params[0];
resultListener = (ResultListener) params[1];
Document doc = null;
DocumentBuilderFactory dbf =
DocumentBuilderFactory.newInstance();
DocumentBuilder db;
try {
db = dbf.newDocumentBuilder();
doc = db.parse(url);
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
}
NodeList listItem =
doc.getElementsByTagName("item");
ArrayList<RSSItem> itemList =
new ArrayList<RSSItem>();
for(int i = 0; i < listItem.getLength(); i++){
//Title
String title =
listItem.item(i).getChildNodes().item(0).getChildNodes()
.item(0).getNodeValue();
//Link
String urlItem =
listItem.item(i).getChildNodes().item(1).getChildNodes()
.item(0).getNodeValue();
itemList.add(new RSSItem(title, urlItem));
}
return itemList;
}
@Override
protected void onPostExecute
(ArrayList<RSSItem> resultList) {
resultListener.onResult(resultList);
super.onPostExecute(resultList);
}
}
}
Nesta classe definimos uma interface chamada ResultListener (linhas 15-17) com o método onResult(ArrayList), chamado quando os dados forem retornados da consulta feita pela classe AsyncTaskRSS, subclasse de AsyncTask.
AsyncTask é uma recomendação do Android quando há necessidade de trabalhar com threads. Ela nos permite criar uma thread separada (em background) e atualizar os dados da thread de UI posteriormente.
Cabe ressaltar que toda e qualquer operação que possa levar muito tempo em sua execução deve ser processada em uma thread, evitando assim problemas de ANR (Application Not Responding).
Diante disso, para criar uma thread devemos estender a classe AsyncTask e definir três tipos genéricos. O primeiro representa quais objetos serão passados para a execução da tarefa, o segundo indica o progresso da tarefa que está sendo executada e o terceiro define o que será recebido como resultado da operação. Caso um dos parâmetros não seja utilizado, deve-se definir o seu tipo como Void. Além disso, é preciso sobrescrever o método doInBackground(), onde são feitas as operações que podem durar muito tempo, dentre elas requisições web.
Para receber o resultado do processamento efetuado pela thread, faz-se necessário reescrever o método onPostExecute(), executado após o término do doInBackground().
No AsyncTaskRSS (linhas 25-70), utilizamos Object como o primeiro tipo genérico. Nele recebemos uma String que representa a URL do RSS e um ResultListenerpara notificar quando a resposta da busca dos dados for obtida. Como não estamos exibindo o progresso da tarefa para o usuário, o segundo tipo foi definido como Void. E para o terceiro tipo, utilizamos um ArrayList, que possui o resultado da consulta.
Tendo em vista que uma consulta a um RSS nada mais é que uma leitura de um XML, precisamos extrair os dados do arquivo de modo que possamos exibi-los para o usuário. Assim, utilizamos a API DOM (Document Object Model) para manipular o documento XML. Nela, para realizar o parser, primeiro obtemos uma instância de DocumentBuilderFactory a partir do método newInstance().
Após isso, criamos um DocumentBuilder por meio do método newDocumentBuilder() de DocumentBuilderFactory. A classe DocumentBuilder tem um método chamado parser()que recebe a URL ou o local do arquivo XML e retorna uma instância da classe Document, que representa um novo objeto DOM com os dados.
Com a instância de um objeto Document em mãos (linha 40), podemos obter um NodeList através do método getElementsByTagName(String). O NodeList contém a lista de itens do RSS, onde cada item é composto por Título e URL. Após isso, iteramos sobre a lista (linhas 52 a 60) e na iteração invocamos o método getChildNodes() para obter todos os filhos de cada item e seus respectivos valores através do método getNodeValue().
De posse do título e da URL da notícia, adicionamos os dados em uma lista de objetos RSSItem e a retornamos em seguida (linhas 54 a 62).
Após o término da execução do doInBackground(), o método onPostExecute() é invocado recebendo a lista de resultados. Essa lista é passada para o listener por meio do método onResult(ArrayList).
Por fim, na classe RSS implementaremos o método getNews(String, ResultListener). Este receberá a URL e a implementação do listener que ouvirá e receberá os dados consultados no feed. Dentro desse método instanciamos a classe interna AsyncTaskRSS e chamamos o método execute() para iniciar a thread.
Definindo os fragmentos da aplicação
Agora que temos a infraestrutura para buscar as informações, iremos implementar os fragmentos. Como informado anteriormente, criaremos dois deles: um que será a lista de notícias, o qual chamamos de FragmentRSSList, e outro chamado de FragmentDetail, que exibirá o título e a página da notícia.
Para construir os fragmentos, basicamente iremos criar classes que estendam android.support.v4.app.Fragment da API de compatibilidade e, após isso, reescrever o método onCreateView(), carregando o layout do fragmento a partir de um arquivo de recurso XML.
Criando o FragmentRSSList
Para começar, criaremos o layout do fragmento responsável por exibir a lista de notícias disponibilizada pelo RSS. Esse layout será composto por um simples LinearLayout com um ListView, como apresentado na Listagem 8.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+fragmentView/listView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</ListView>
</LinearLayout>
Em seguida, implementaremos a classe FragmentRSSList, que controlará o funcionamento da listagem de notícias. A Listagem 9 apresenta o código desta classe.
1. package br.com.jm.fragments;
2.
3. import java.util.ArrayList;
4. import android.os.Bundle;
5. import android.support.v4.app.Fragment;
6. import android.view.LayoutInflater;
7. import android.view.View;
8. import android.view.ViewGroup;
9. import android.widget.AdapterView;
10. import android.widget.AdapterView.OnItemClickListener;
11. import android.widget.ArrayAdapter;
12. import android.widget.ListView;
13. import br.com.jm.fragments.rss.RSS;
14. import br.com.jm.fragments.rss.RSS.ResultListener;
15. import br.com.jm.fragments.rss.RSSItem;
16.
17. public class FragmentRSSList extends Fragment implements
ResultListener {
18.
19. private ItemListener mItemListener;
20.
21. public interface ItemListener {
22. public void onClickItem(RSSItem item);
23. }
24.
25. @Override
26. public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
27. View view = inflater.inflate(R.layout.list_rss_fragment,
container, false);
28. return view;
29. }
30.
31. public void loadNews(ItemListener itemListener){
32. this.mItemListener = itemListener;
33. RSS rss = new RSS();
34. rss.getNews
("http://g1.globo.com/dynamo/rss2.xml", this);
35. }
36.
37. public boolean isInTablet(){
38. if (getActivity().getResources().getBoolean
(R.bool.isTablet))
39. return true;
40. else
41. return false;
42. }
43.
44. @Override
45. public void onResult(ArrayList<RSSItem> resultList) {
46. ArrayAdapter<RSSItem> adapter =
new ArrayAdapter<RSSItem>(getActivity(),
android.R.layout.simple_list_item_1, resultList);
47. ListView listView = (ListView)
getView().findViewById(R.fragmentView.listView);
48. listView.setAdapter(adapter);
49. listView.setOnItemClickListener(new OnItemClickListener() {
50. @Override
51. public void onItemClick(AdapterView<?> adapterView,
View view, int position, long arg3) {
52. RSSItem rssItem = (RSSItem)
adapterView.getItemAtPosition(position);
53.
54. mItemListener.onClickItem(rssItem);
55. }
56. });
57. }
58. }
Na construção do fragmento FragmentRSSList, exibido na Listagem 9, primeiramente definimos uma interface que representará um listener, chamada ItemListener (linhas 21 a 23). Ela será necessária para notificar a aplicação quando os dados consultados forem retornados. Assim, toda e qualquer activity que deseje utilizar o fragmento para listar notícias terá que implementar o método onClickItem(RSSItem), recebendo o objeto RSSItem que foi selecionado pelo usuário.
Após isso criaremos o método loadNews(ItemListener), que receberá o listener que será disparado pelo fragmento quando algum item da lista for selecionado (linhas 31 a 35). Esse ItemListener será guardado em um membro da classe (linha 32) para ser usado mais à frente. Em seguida instanciamos a classe RSS invocando o método getNews(), que recebe dois parâmetros. São eles:
- String url: URL do RSS na qual devem ser buscadas as notícias;
- ResultListener resultListener: Classe que contém um método que será invocado quando a consulta retornar os dados.
Para passarmos um ResultListener para o método getNews()é necessário que nossa classe FragmentRSSList implemente a interface ResultListener e reescreva o método onResult(ArrayList), que receberá a lista de itens do RSS retornados pelo feed. Estes itens são usados para criar um Adapter que posteriormente será atribuído ao componente ListView do nosso fragmento, para serem exibidos na tela (linhas 45 a 57).
Agora devemos identificar quando um item do ListView foi selecionado. Para isso, implementamos o método setOnItemClickListener(OnItemClickListener) do ListView por meio de uma classe anônima e recuperamos o RSSItem clicado (linhas 49 a 56). Em seguida, notificamos a nossa interface ItemListener por meio do método onClickItem(RSSItem), e passamos o item de RSS que foi clicado no fragmento para a activity.
No intuito de diferenciar se o fragmento está em um Tablet ou não, a classe FragmentRSSList possui o método isInTablet(), que permite carregar um layout conforme as características do dispositivo (linhas 37 a 42).
Após a implementação da classe FragmentRSSList, podemos adicioná-la em qualquer tela da aplicação. Como dito anteriormente, neste exemplo utilizaremos fragmentos declarados em XML, porém também é possível adicionar e substituir fragmentos em tempo de execução por meio da API de fragmentos. A Listagem 10 mostra como inserir o fragmento na tela principal da aplicação (layout/activity_main.xml). Esse arquivo será usado como layout no smartphone.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/fragment_list"
android:name="br.com.jm.fragments.FragmentRSSList"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:tag="fragment_list" />
</RelativeLayout>
Criando o FragmentDetail
Construiremos agora o fragmento responsável por exibir a descrição da notícia e a página da mesma na tela. A Listagem 11 mostra como ficará o layout dos detalhes da notícia, composto por um TextView e um WebView.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView android:id="@+id/txtTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<WebView
android:id="@+id/webViewDetail"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
Após a definição do layout, implementaremos a classe responsável por receber o título e a URL da notícia e exibi-los. A Listagem 12 apresenta como ficará a classe FragmentDetail.
package br.com.jm.fragments;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.TextView;
import br.com.jm.fragments.rss.RSSItem;
public class FragmentDetail extends Fragment {
private WebView webView;
private TextView txtTitle;
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate
(R.layout.detail_fragment, container, false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
webView = (WebView) getView().findViewById
(R.id.webViewDetail);
txtTitle = (TextView) getView().findViewById
(R.id.txtTitle);
}
public void loadData(RSSItem rssItem){
webView.loadUrl(rssItem.getUrl());
txtTitle.setText(rssItem.getTitle());
}
}
Após estendermos a classe Fragment e inflarmos a view do nosso layout, implementamos o método onActivityCreated(Bundle). Este indica que a activity que contém o fragmento já foi criada. Dessa forma podemos recuperar os componentes do layout pelo ID. Em seguida, criamos o método loadData(RSSItem), que recebe um item de RSS como parâmetro. Assim, podemos invocar os métodos setText(String) e loadUrl(String) dos componentes (linhas 31 e 32) para atribuir os valores vindos no parâmetro RSSItem.
Fragmentos em Tablets
Como citado anteriormente, o layout que será exibido no smartphone é diferente do apresentado no Tablet. Portanto, para diferenciarmos os layouts, é necessário criar pastas de recursos diferentes. No nosso caso, criamos a pasta layout-xlarge e colocamos nela o layout descrito na Listagem 13 (layout-xlarge/activity_main.xml).
Para produzirmos uma exibição diferente em um tablet, codificamos na Listagem 13 dois fragmentos, o fragmento da lista e o fragmento de detalhe, na mesma tela. Colocamos cada declaração desses fragmentos dentro de um LinearLayout e utilizamos o atributo android:layout_weight="1" para dividir a tela ao meio.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">
<fragment
android:id="@+id/fragment_list"
android:name="br.com.jm.fragments.FragmentRSSList"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:tag="list_tablet"/>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">
<fragment
android:id="@+id/fragment_detail"
android:name="br.com.jm.fragments.FragmentDetail"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:tag="detail"/>
</LinearLayout>
</LinearLayout>
Construindo as activities da aplicação
Nos tópicos anteriores, ensinamos como criar os fragmentos e os layouts que serão usados na aplicação. Agora, será apresentado o código das activities empregadas no leitor de notícias. Elas permitem que a lógica de exibição seja diferente em smartphones e tablets utilizando o conceito de fragmentos.
A classe MainActivity
Para a tela principal, criamos a classe MainActivity, que é responsável por listar as notícias. Como pode ser observado, ela estende FragmentActivity para que tenhamos acesso ao método getSupportFragmentManager(). A Listagem 14 mostra a implementação dessa classe, onde após setarmos o layout, na linha 6, utilizamos o método getSupportFragmentManager(), que retorna um objeto que disponibiliza alguns métodos para recuperação de fragmentos na tela, dentre eles o findFragmentById(int idRes).
Logo após, verificamos se o fragmento não é nulo e se o mesmo foi encontrado no layout (linha 16). Em seguida invocamos o método loadNews(ItemListener), responsável por buscar os dados no feed, e implementamos uma classe anônima para decidir o que deve ser feito quando um item for selecionado.
A implementação da classe anônima (linhas 11 a 25) leva em consideração se o FragmentRSSList está sendo executado em um smartphone ou em um tablet. Para isso utilizamos o método isInTablet() da classe FragmentRSSList. Caso seja um tablet, devemos recuperar a instância de FragmentDetail e invocar o método loadData(RSSItem)(linhas 14 a 18), que por sua vez receberá os parâmetros e cuidará de exibir os dados. Porém, caso não seja um tablet, a aplicação terá que abrir uma activity que contém o FragmentDetail. Dessa forma, iniciamos a activity DetailActivity e passamos para ela o objeto RSSItem (linhas 20 a 22). Com isso conseguimos diferenciar os layouts de smartphones e tablets, possibilitando o reuso de componentes a partir dos fragmentos criados.
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final FragmentRSSList fragmentRSSList = (FragmentRSSList)
getSupportFragmentManager()
.findFragmentById(R.id.fragment_list);
if (fragmentRSSList != null &&
fragmentRSSList.isInLayout()){
fragmentRSSList.loadNews
(new FragmentRSSList.ItemListener() {
@Override
public void onClickItem(RSSItem item) {
if (fragmentRSSList.isInTablet()){
FragmentDetail fragmentDetail = (FragmentDetail)
getSupportFragmentManager()
.findFragmentById(R.id.fragment_detail);
if (fragmentDetail != null &&
fragmentDetail.isInLayout()){
fragmentDetail.loadData(item);
}
}else{
Intent intent = new Intent(MainActivity.this,
DetailActivity.class);
intent.putExtra(RSSItem.RSS_ITEM_PARAM, item);
startActivity(intent);
}
}
});
}
}
}
A classe DetailActivity
Quando acessarmos a aplicação de um smartphone, ela terá duas activities: uma que mostrará a lista de notícias e a outra os detalhes da notícia. A Listagem 15 apresenta a implementação da classe responsável por mostrar o detalhamento das notícias em um smartphone.
package br.com.jm.fragments;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import br.com.jm.fragments.rss.RSSItem;
public class DetailActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
}
@Override
protected void onStart() {
super.onStart();
FragmentDetail fragmentDetail = (FragmentDetail)
getSupportFragmentManager().findFragmentById
(R.id.fragment_detail);
if (fragmentDetail != null && fragmentDetail.isInLayout()){
Bundle bundle = getIntent().getExtras();
if (bundle != null){
RSSItem rssItem = (RSSItem)
bundle.getSerializable(RSSItem.RSS_ITEM_PARAM);
fragmentDetail.loadData(rssItem);
}
}
}
}
Além de estendermos a classe FragmentActivity, implementamos dois métodos: onCreate(Bundle) e onStart(). No método onCreate(), simplesmente inflamos o layout que contém o fragmento DetailFragment declarado. No método onStart(), exibido entre as linhas 14 e 28, obtemos a instância de DetailFragment e os parâmetros enviados pela Intent através do método getExtras() do objeto Bundle. Desta forma, conseguimos recuperar o RSSItem passado e assim invocar o método loadData(RSSItem) enviando o item como parâmetro.
Criando fragmentos dinamicamente
Existem duas formas de definir um fragmento: por meio da tag <fragment> que é usada no arquivo XML de layout e através da API de fragmentos. Nesta, é possível adicionar, remover ou substituir um fragmento em tempo de execução. Para isto, devemos utilizar as classes FragmentManager e FragmentTransaction da API de fragmentos.
A classe FragmentManager fornece uma instância de FragmentTransaction através do método beginTransaction(). De posse da transação, pode-se adicionar, remover ou substituir um fragmento em um layout qualquer da tela invocando os métodos add(), remove() e replace() da própria FragmentTransaction. Por fim, o método commit(), também da classe FragmentTransaction, deve ser chamado para confirmar a operação.
Para demonstrar como adicionar um fragmento em tempo de execução, a Listagem 16 apresenta um trecho de código exemplificando o uso do método add() de FragmentTransaction.
FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container,
fragment, "exemploFragment");
fragmentTransaction.commit();
No código da Listagem 16, como já informado, o método getFragmentManager() devolve uma instância de FragmentManager. Esse método está presente nas classes Activity e Fragment e, portanto, qualquer subclasse destas tem acesso a ele. Em seguida, um FragmentTransaction é retornado pela chamada ao método beginTransaction() de FragmentManager. Após isso, o fragmento ExampleFragment é instanciado e adicionado dinamicamente à Activity depois da chamada ao método add() de FragmentTransaction. No final, o método commit() confirma a operação de adição do fragmento.
Como pode ser verificado, a versão do método add() que está sendo usada possui três parâmetros. No primeiro, deve ser informado o identificador do layout onde o fragmento será adicionado. No segundo, temos uma instância da classe Fragment, que representa o fragmento a ser adicionado. Por último, temos uma String descrevendo o nome da tag, um identificador opcional do fragmento, para que seja possível encontrá-lo futuramente utilizando o método findFragmentByTag(tag).
O método add() possui também uma versão com dois parâmetros (o layout e o fragmento). Ele tem o mesmo efeito de invocar a versão com três parâmetros passando o valor null no parâmetro da tag. Neste caso, no entanto, não será possível achar o fragmento por meio do método findFragmentByTag(tag).
Caso o desenvolvedor deseje remover um fragmento de uma Activity, isso pode ser feito obtendo-se uma instância de FragmentManager, que por sua vez fornece um FragmentTransaction, pelo qual acessamos o método remove(). Este método recebe como parâmetro apenas um objeto do tipo Fragment, representando o fragmento a ser removido. Após isso, a chamada ao método commit() assegura a remoção (ver Listagem 17).
FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
ExampleFragment fragment = // recupera o fragmento a ser removido
fragmentTransaction.remove(fragment);
fragmentTransaction.commit();
Além da remoção, é possível a substituição de um fragmento dinamicamente, como mostra a Listagem 18.
FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.replace(R.id.fragment_container, fragment,
"exemploFragment");
fragmentTransaction.commit();
A sequência de chamadas de métodos, no caso da substituição de um fragmento por outro, é exatamente a mesma usada para adicionar ou removê-lo. A única diferença é que, ao invés de chamar o método add() ou remove(), invocamos o método replace(). A versão desse método empregada no exemplo recebe três parâmetros, assim como o add().
São eles: o identificador do layout, onde se encontra o fragmento a ser substituído e o novo fragmento deve ser mostrado, a instância do novo fragmento e uma tag para que seja possível encontrá-lo depois.
Há também outra versão do método replace(), apenas com dois parâmetros: o identificador do layout e o novo fragmento. Este método, internamente, chama a versão do replace() com três parâmetros, passando o valor null no parâmetro da tag.
Fragment Back Stack
O comportamento padrão do Android ao pressionar o botão voltar é de destruir a activity que está sendo mostrada, removendo-a da pilha de activities. Caso a activity contenha fragmentos, eles consequentemente também serão destruídos.
No entanto, às vezes é necessário que o botão voltar simplesmente desfaça uma operação efetuada pelo FragmentTransaction, antes de fechar a activity atual. Para casos assim, o Android permite adicionar cada transação em uma pilha de controle para o botão voltar, a back stack. Com isso, o botão voltar primeiro desfaz as transações realizadas, até que não haja mais nenhuma transação na pilha de controle, e em seguida, fecha a activity. O método addToBackStack() de FragmentTransaction é usado para adicionar a transação na back stack.
Como visto, uma transação, representada pela classe FragmentTransaction, consiste em um conjunto de mudanças aplicado aos fragmentos de uma activity ao mesmo tempo, tais como: adição, remoção ou substituição de fragmentos. Tais mudanças só são efetivadas mediante a chamada ao método commit().
No entanto, para colocar a transação na back stack, antes do commit(), o método addToBackStack() deve ser invocado. Isto permite que as alterações da transação sejam desfeitas ao acionar o botão voltar. Veja a Listagem 19.
FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
ExampleFragment newFragment = new ExampleFragment();
fragmentTransaction.replace(R.id.fragment_container,
newFragment, "exemploFragment");
fragmentTransaction.addToBackStack ("replaceFrag");
fragmentTransaction.commit();
Nesse trecho de código, o fragmento newFragment substitui qualquer outro que esteja adicionado no layout cujo identificador é R.id.fragment_container. A chamada ao método addToBackStack() faz com que a operação de substituição seja inserida na back stack para que o usuário possa reverter a transação e trazer de volta o fragmento anterior pressionando o botão voltar.
Vale lembrar que ao chamar o addToBackStack(), todas as mudanças (adicionar, remover ou substituir fragmentos) feitas na transação são inseridas na back stack como uma simples transação, de forma que o acionar do botão voltar irá desfazer todas as alterações de uma só vez. O método addToBackStack(name) recebe um parâmetro opcional do tipo String que serve para identificar a transação na back stack. Para este parâmetro pode ser atribuído o valor null caso não se deseje identificar a transação.
É importante salientar que o fato de não chamar o método addToBackStack() ao efetuar uma operação de remoção de fragmento, implica na destruição deste caso haja uma confirmação da mesma, por meio do commit(). Isto faz com que não seja mais possível navegar por esse fragmento. Por outro lado, se o método addToBackStack() for chamado antes de confirmar a remoção de um fragmento, então o fragmento é apenas interrompido e pode ser retomado quando necessário.
Conclusão
O crescente número de aparelhos Android faz com que os desenvolvedores se preocupem em criar aplicativos que se adaptem aos mais variados tamanhos de tela. Para auxiliá-los nessa tarefa, foi criada a API de fragmentos. Os fragmentos são componentes reutilizáveis que possuem um ciclo de vida próprio e podem ser programados para adaptar-se a vários tipos de tela.
Com base nesse contexto, este artigo abordou essa API e como ela pode ser usada para criar aplicações que tenham seus layouts adaptáveis em vários tamanhos de tela. Além disso, foi apresentado como criar um fragmento, tanto via declaração no arquivo XML de layout quanto dinamicamente via código, o ciclo de vida do fragmento, a relação deste com o ciclo de vida da activity que o adicionou e como utilizar a biblioteca de compatibilidade, que permite usar a API de fragmentos em versões do Android mais antigas, visto que a API de fragmentos surgiu na versão 3.x (Honeycomb).
Com objetivo de assimilar esses conceitos e mostrar na prática como utilizá-los, foi desenvolvida uma aplicação que usa fragmentos. Nela, o usuário pode navegar em uma lista com títulos de notícias obtidas de um RSS e, ao clicar em uma notícia, são exibidos os seus detalhes. Essa aplicação foi projetada para funcionar em dois dispositivos com telas de tamanhos diferentes: um tablet e um smartphone.
Para isso, foi necessária a criação de dois fragmentos: um responsável pelo layout que exibirá a lista com os títulos das notícias e outro com o layout para exibir os detalhes da notícia selecionada. No caso do tablet, como este possui uma grande área útil para a aplicação, ela coube em apenas uma activity, exibindo à esquerda o fragmento da lista de títulos e à direita o fragmento com os detalhes. Já no smartphone, como a tela é menor, foram utilizadas duas activities, uma para exibir a lista de notícias e outra para exibir os detalhes da notícia desejada.
A grande vantagem, neste caso, é que tanto no tablet quanto no smartphone foram usados os mesmos fragmentos, apenas dispostos de formas diferentes, aumentando assim o reuso e evitando a duplicação de código.