terça-feira, 31 de janeiro de 2012

Fragmentos para todos

Olá povo,

Uma das novidades do Android 3 (Honeycomb), foi a adição do conceito de Fragments. Com essa versão foi escrita exclusivamente para tablets, os "fragmentos" permitem a separação de uma Activity em pedaços de modo a aproveitar os grande tamanho de tela desse tipo de dispositivo. Os Fragments funcionam como sub-activities (dentro de uma Activity), e têm seu próprio ciclo de vida, tratando também os eventos dos componentes nele contidos.

Você pode estar se perguntando: ótimo, mas isso é só pro Android 3 ou superior, então não roda no 2.x. Você estaria certo se a Google não tivesse disponibilizado uma API de compatibilidade, que permite usufruir de alguns recursos do Honeycomb em aparelhos 1.6 (Donut) ou superior. Essa biblioteca está disponível no Android SDK Manager, o mesmo lugar onde você baixa os pacotes necessários para o desenvolvimento Android. Essa API é basicamente um JAR que deve ser adicionado no seu projeto Android.

A Figura abaixo mostra a opção para baixar a API de compatibilidade no Android SDK Manager.

EDITADO EM 10/08/2012
Na versão atual do plugin, essa lib é adicionada automaticamente ao projeto.

Uma vez baixado, o JAR vá até a pasta do SDK e procure pelo arquivo android-support-v4.jar. Ele pode estar na pasta extras/android/compatibility/v4 ou android-compatibility/v4 (dependendo do S.O.).
Crie um novo projeto Android selecionando a versão 1.6 ou superior, e na raiz do projeto, crie uma pasta lib. Copie o JAR para essa pasta e depois adicione-o ao Build-Path do projeto (botão direito sobre JAR, Build Path > Add to build path). Em seguida, clique com o botão direito sobre o projeto e clique em propriedades. Por fim, selecione Java Build Path e na aba Order and Export, marque o android-support-v4.jar.

Antes de começarmos a codificar, vou explicar como será o exemplo. Será uma aplicação clássica de Fragments, onde teremos uma lista, e ao clicar em um dos elementos dessa lista, os detalhes daquele item serão exibidos. Como exemplo, listarei as versões do Android e ao clicar, exibir os detalhes da respectiva versão.

Vou começar criando o layout principal da aplicação. Crie o arquivo res/layout/titles.xml e deixe-o conforme abaixo:
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="horizontal" >

  <fragment
    android:id="@+id/titles"
    android:layout_width="0px"
    android:layout_height="fill_parent" 
    android:layout_weight="1"
    class="ngvl.android.fragments2.TitlesFragment" />

  <FrameLayout
    android:id="@+id/details"
    android:layout_width="0px"
    android:layout_height="fill_parent"
    android:layout_weight="1" />
</LinearLayout>

Aqui temos um LinearLayout horizontal com um Fragment que está apontando para a classe TitlesFragment e está ocupando metade da tela (determinado pela propriedade layout_weight). E um FrameLayout para exibir os detalhes da opção selecionada.

O arquivo acima dará erro porque não criamos a classe TitlesFragment. Ela é mostrada abaixo:
public class TitlesFragment extends ListFragment {

  private final String EXTRA_INDEX = "index";
  private int selectedIndex;
 
  @Override
  public void onActivityCreated(
    Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
  
    ListView lv = getListView();
  
    setListAdapter(
      new ArrayAdapter<String>(
      getActivity(),
      android.R.layout.simple_list_item_checked,
      Dados.titles));
  
    lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
  
    if (savedInstanceState != null){
      selectedIndex = 
        savedInstanceState.getInt(EXTRA_INDEX);
      lv.setSelection(selectedIndex);
    }

    if (isDualPane()){
      showDetails(selectedIndex);
    } else {
      getActivity().findViewById(
        R.id.details).setVisibility(View.GONE);
    }
  
    lv.setItemChecked(selectedIndex, true);  
  }

  private void showDetails(int index) {
    selectedIndex = index;
  
    if (isDualPane()) {  
      DetailsFragment details = 
        (DetailsFragment)getFragmentManager()
          .findFragmentById(R.id.details);
   
      if (details == null || 
        details.getShowIndex() != index){

        details = DetailsFragment.newInstance(index);
    
        FragmentTransaction fragTrans =
          getFragmentManager().beginTransaction();
        fragTrans.replace(R.id.details, details);
        fragTrans.setTransition(
          FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        fragTrans.commit();
      }
    } else {
      Intent it = new Intent(getActivity(), 
        DetailsActivity.class);

      it.putExtra(EXTRA_INDEX, index);
      startActivity(it);
    }
  }
 
  @Override
  public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt(EXTRA_INDEX, selectedIndex);
  }
 
  @Override
  public void onListItemClick(ListView l, 
    View v, int position, long id) {
    super.onListItemClick(l, v, position, id);
    showDetails(position);
  }
 
  private boolean isDualPane(){
    Configuration config =
      getActivity().getResources().getConfiguration();

    return config.orientation == 
      Configuration.ORIENTATION_LANDSCAPE;
  }
}

Nosso fragmento herda de ListFragment, que é bem similar a classe ListActivity, e exibe informações em formato de lista. Então, no método onActivityCreated (o nome por sí só já diz o que ele faz :) obtemos a referência para a ListView e setamos um Adapter para ela. Os dados que são passados para esse Adapter vêm de um array de Strings contendo as versões do Android definidos na classe Dados. Parte dessa classe é mostrada abaixo:
public class Dados {
  static public String[] titles = {
    "Android 1.5",
    "Android 1.6",
    // Mais itens aqui
  }

  static public String[] description = {
    "Primeira versão do Android, Cupcake",
    "Donut já suporta Fragments",
    // Mais descrições aqui
  }
}

Em seguida, verificamos se existe um estado salvo da nossa Activity. Quando entramos a primeira vez, esse estado estará vazio, mas quando girarmos o aparelho, o método onSaveInstance será chamado e nós salvaremos a posição da lista que está checada. Se já existir um estado saldo, marcamos a opção que estava selecionada antes de girar o aparelho e fazemos o scroll até ela.
Logo após, verificamos se a aplicação é dualPane (dois painéis) esse método (que está no fim da classe) verifica se a orientação do aparelho está como landscape. Em caso positivo exibiremos a lista e os detalhes no mesmo momento, caso contrário exibiremos apenas a lista, então ocultamos o FrameLayout de detalhes.
O método showDetails verifica se a tela é dualPane, em caso positivo, instancia o fragmento de detalhes e o exibe utilizando a class FragmentTransition. Caso não seja dualPane, ou seja, está em Portrait, exibimos a Activity de detalhes.

Com a utilização de Fragments, a Activity passa a controlar o Fragment, e o Fragment trata eventos de UI. Com isso a Activity fica bem simples como podemos ver abaixo:
public class TitleActivity extends FragmentActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.titles);
  }
}

O único detalhe é que essa clase herda de FragmentActivity. Lembrando que ela é uma classe da API de compatibilidade, no Android 3.x podemos continuar usando Activity mesmo. Agora vamos analisar o fragmento de detalhes
public class DetailsFragment extends Fragment {

  private static final String EXTRA_INDEX = "index";
 
  public static DetailsFragment newInstance(int index) {
    DetailsFragment fragment = new DetailsFragment();
  
    Bundle params = new Bundle();
    params.putInt(EXTRA_INDEX, index);
    fragment.setArguments(params);
  
    return fragment;
  }
 
  public int getShowIndex() {
    return getArguments().getInt(EXTRA_INDEX);
  }

  @Override
  public View onCreateView(LayoutInflater inflater,
    ViewGroup container, Bundle savedInstanceState) {
  
    TextView txt = new TextView(getActivity());
    txt.setText(Dados.description[getShowIndex()]);
  
    return txt;
  }
}

A classe tem um Factory Method (chamado newInstance) que cria um DetailsFragment já passando os "extras" necessários. O método onCreateView define o que será exibido no Fragment, para simplificar, criamos apenas um TextView, mas você poderia usar o parâmetro inflater para carregar um arquivo de Layout que desejasse.
Por fim, vamos mostrar a Activity de detalhes.
public class DetailsActivity extends FragmentActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  
    if (isDualPane()){
      finish();
      return;
   
    } else if (savedInstanceState == null) {
  
      DetailsFragment details = new DetailsFragment();
      details.setArguments(getIntent().getExtras());
   
      getSupportFragmentManager()
        .beginTransaction()
        .add(android.R.id.content, details)
        .commit();
    }
  }
 
  private boolean isDualPane(){
    Configuration config =
      getResources().getConfiguration();

    return config.orientation ==
      Configuration.ORIENTATION_LANDSCAPE;
  }
}

Nessa Activity, basicamente verificamos se a orientação é landscape, em caso positivo, essa Activity é fechada, e consequentemente a TitlesActivity é exibida mostrando os títulos e os detalhes do mesmo. Já se Activity não é dual pane (ou seja, é portrait) e foi passado o índice para ela, carregamos o Fragment e o adicionamos à tela.

A imagem abaixo mostra nossa aplicação rodando em landscape.

Esse artigo foi baseado nesse post do Blog do Android Developers.

Qualquer dúvida, deixem seus comentários,

4br4ç05,
nglauber

2 comentários:

cohen disse...

Olá Glauber tudo bem?

Estou praticando os exercicios do seu livro e fiquei na duvida pq nao conseguir avancar... No exemplo 17 que tem dois niveis de fragments tem um textView e gostaria de colocar a ListView do Adapter do Exemplo 10. Como faço pra inserir a list no fragment, vi os exemplos que mostra como colocar a list na activity mas no fragment nao é igual. Como seria? vc poderia me ajudar.

Parabens pelo livro, muito bom!!!

Nelson Glauber disse...

Oi Cohen,

Que bom que está gostando do livro. Aproveita e se inscreve no grupo de discussão.
https://groups.google.com/forum/#!forum/livro-dominando-o-android

Em relação a sua dúvida, no exemplo, onde tiver o SegundoNivelFragment, usa o seu Fragment.

4br4ç05,
nglauber