segunda-feira, 2 de maio de 2016

Android Data Binding

Olá pessoal,

A API de DataBinding foi lançada no Google I/O de 2015 e tem o intuito de facilitar a vida dos desenvolvedores removendo da Activity/Fragment muita da lógica de UI feitas por esses componentes. Apesar de ter sido lançado juntamente com o Android 6, a API de Data Binding é uma biblioteca separada do sistema operacional e pode ser utilizada a partir do Android 2.1 (API Level 7). Três coisas que por si só já valem a utilização do Data Binding são: (1) não utilizar findViewById [tchau ButterKnife], (2) associação de eventos a componentes e (3) sincronização de valores do model para a view.

Apesar desses três pontos já serem fantásticos, o Data Binding tem muito mais a oferecer. O objetivo desse post é mostrar como dar os primeiros passos com essa API por meio de um exemplo simples, mas funcional. O código-fonte desse exemplo está disponível no meu github, por isso, vou omitir algumas partes do código que não estão relacionadas ao Data Binding.

Classes básicas e configuração

O exemplo consta de uma listagem de livros utilizando a API do Google Books. Então defini duas classes básicas: Book e Thumbnail.
// Thumbnail.java
@Parcel
public class Thumbnail {
    private String smallThumbnail;
    private String thumbnail;
    // gets e sets...
}

// Book.java
@Parcel
public class Book {
    private String title;
    private String subtitle;
    private String publisher;
    private String description;
    private String[] authors;
    private String publishedDate;
    private int pageCount;
    private Thumbnail imageLinks;
    // gets e sets
}

A única observação sobre essa classe é que estou utilizando a biblioteca Parceler que facilita a implementação do Parcelable do Android. No mais, são classes Java simples.

A única configuração que deve ser feita no projeto Android é adicionar o recurso de Data Binding ao build.gradle do módulo da sua aplicação.
android {
    ...

    dataBinding {
        enabled = true
    }
Pronto! Podemos começar a implementar a UI do nosso projeto.

Um adapter utilizando Data Binding

A primeira tela do exemplo exibe uma listagem de livros retornadas pela API do Google Books. No nosso exemplo, exibiremos a capa, título e os autores do livro. Para exibir essa listagem, usaremos um adapter, no qual o arquivo de layout utilizado para representar cada linha da lista é exibido a seguir (res/layout/item_book.xml).
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="java.util.Arrays"/>
        <variable
            name="book"
            type="nglauber.android.databinding.model.Book" />
    </data>

    <RelativeLayout ... >
        <ImageView ...
            android:id="@+id/image_capa"
            android:src="@{book.imageLinks.smallThumbnail}"/>
        <TextView ...
            android:id="@+id/text_titulo"
            android:text="@{book.title}" />
        <TextView ...
            android:id="@+id/text_autor"
            android:text="@{Arrays.toString(book.authors)}" />
    </RelativeLayout>
</layout>

Os arquivos de layout que utilizam o recurso de Data Binding devem iniciar com a tag <layout>. Dentro da tag <data> devemos declarar as variáveis e importar as classes que serão utilizadas no arquivo de layout. Perceba que estamos declarando uma <variable> chamada "book" da classe Book que criamos anteriormente.
No TextView que exibirá o título do livro, setamos a propriedade android:text com o valor @{book.title}. Desta forma, o título do livro será exibido automaticamente no componente.
Ótimo, mas se você notar na nossa classe Book, o atributo "authors" é um array de strings. Para exibir os autores separado por vírgula, podemos utilizar a classe java.utils.Arrays. Mas como usá-las dentro de um arquivo de layout? Basta importamos essa classe por meio da tag <import>.
Outro ponto curioso nesse arquivo é que no ImageView estamos definindo na propriedade android:src a imagem que será exibida. Mas se você desenvolve em Android (nem que seja a um pouquinho de tempo) deve lembrar que nessa propriedade devemos passar uma imagem que está dentro do projeto (um Drawable normalmente) e não uma URL. Mas no nosso exemplo, temos apenas o link da imagem. E agora?
Com data binding, podemos definir adapters para propriedades utilizando BinderAdapters! Vejamos a classe a seguir:

public class ImageBinding {
    @BindingAdapter({"android:src"})
    public static void loadImage(ImageView imageView, String url){
        Glide.with(imageView.getContext()).load(url).into(imageView);
    }
}
Com a anotação @BindingAdapter, informamos que estamos tratando a propriedade android:src recebendo a url como parâmetro. O carregamento da imagem é feito utilizando a biblioteca Glide. Tudo isso é feito "automagicamente" pelo plugin do Data Binding! Muito bom hein!?
Veremos agora como utilizar esse arquivo de layout em um adapter.
public class BookAdapter extends 
        RecyclerView.Adapter<BookAdapter.ViewHolder> {

    List<Book> mBooks;
    BookClickListener mListener;

    public BookAdapter(List<Book> books,
                       BookClickListener listener) {
        mBooks = books;
        mListener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, 
                                         int viewType) {
        ItemBookBinding binding = DataBindingUtil.inflate(
                LayoutInflater.from(parent.getContext()),
                R.layout.item_book,
                parent,
                false);

        final ViewHolder vh = new ViewHolder(binding);
        vh.itemView.setOnClickListener(... );
        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int pos) {
        Book book = mBooks.get(pos);
        holder.binding.setBook(book);
        holder.binding.executePendingBindings();
    }

    @Override
    public int getItemCount() {
        return mBooks != null ? mBooks.size() : 0;
    }

    public static class ViewHolder extends
            RecyclerView.ViewHolder {

        ItemBookBinding binding;

        public ViewHolder(ItemBookBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }
}
Esse é um adapter de uma RecyclerView, se você ainda não mexeu com esse componente, dê uma olhada nesse post aqui.
Percebam que na classe ViewHolder, temos uma instância da classe ItemBookBinding. Mas de onde saiu essa classe? Ela é gerada automaticamente pelo plugin do Data Binding e é baseada no nome do arquivo de layout (res/layout/item_book.xml => ItemBook + Binding) e contém todas as Views declaradas nele! ADEUS findViewById!!!
Notem que estamos passando o elemento raiz do arquivo de layout utilizando o método getRoot().
No método onCreateViewHolder, utilizamos o método inflate da classe DataBindingUtil para carregar o arquivo de layout e obter o objeto ItemBookBinding.
Por fim, no método onBindViewHolder, onde devemos preencher as views do layout, nós simplesmente atribuímos o objeto Book do ItemBookBinding (que definimos na tag ) no ViewHolder. O data binding vai preencher todas as View baseado no objeto Book! <3. Para que essa atualização seja feita imediatamente, invocamos o método executePendingBindings().


Fragment de listagem

Para utilizar o adapter que acabamos de criar, crie um novo fragment (BookListFragment) e deixe o arquivo de layout (res/layout/fragment_book_list.xml) como a seguir:
<layout 
    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">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".BookListFragment">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/list_livro"
            android:layout_width="match_parent"
            android:layout_height="match_parent" 
            app:layout_manager='@{"linear"}' />
    </FrameLayout>
</layout>
Temos uma propriedade interessante nesse arquivo de layout: app:layout_manager, pois ela na verdade não existe nativamente! #confuso. Mas assim como fizemos com o carregamento da imagem, vamos utilizar o BinderAdapter para resolver essa propriedade para nós. Veja a classe a seguir.
public class LayoutManagerBiding {
    @BindingAdapter({"bind:layout_manager"})
    public static void setLayoutManager(
            RecyclerView recyclerView, String layout){
        setLayoutManager(recyclerView, layout, 1);
    }
    
    @BindingAdapter({"bind:layout_manager", "bind:columns"})
    public static void setLayoutManager(
            RecyclerView recyclerView, 
            String layout, int columns){

        if ("linear".equalsIgnoreCase(layout)){
            recyclerView.setLayoutManager(
                    new LinearLayoutManager(
                            recyclerView.getContext(), 
                            LinearLayoutManager.VERTICAL, false));
        } else if ("grid".equalsIgnoreCase(layout)){
            recyclerView.setLayoutManager(
                    new GridLayoutManager(
                            recyclerView.getContext(), columns));
        }
    }
} 
Essa classe configurará o layout manager do RecyclerView. Aqui, apenas para fins de exemplo, estamos tratando os layouts linear e de grid. Sendo que para utilizarmos grid, podemos opcionalmente passar a propriedade (também customizada) app:columns.

Vamos agora para a implementação da classe do Fragment.
public class BookListFragment extends Fragment {

    List<Book> mBooks;
    BookAdapter mAdapter;
    BookTask mTask;
    FragmentListBookBinding mBinding;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        mBooks = new ArrayList<>();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, 
                             ViewGroup container,
                             Bundle savedInstanceState) {
        mBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_list_book, container, false);

        mAdapter = new BookAdapter(mBooks, mListener);
        mBinding.listLivro.setAdapter(mAdapter);
        return mBinding.getRoot();
    }

    // Os dados são baixados assincronamente 
    // utilizando a API do Google Books
    public void search(String term){
        mTask = new BookTask(this);
        mTask.execute(term);
    }

    public void setLivros(List<Book> books){
        if (books != null) {
            mBooks.clear();
            mBooks.addAll(books);
        }
        mAdapter.notifyDataSetChanged();
    }

    // ...
}
Assim como fizemos no Adapter utilizamos a classe DataBindingUtil para carregar o arquivo de layout e obter a instância da "binding class" FragmentListBookBinding.

Activity principal

Na activity principal, temos um campo onde o usuário poderá digitar o nome do livro a ser pesquisado. Ao clicar no botão iniciamos que é processada no fragment de listagem.
Vejamos o arquivo de layout (activity_book.xml)
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="handler"
            type="nglauber.android.databinding.BookActivity"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".BookActivity">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/edit_search"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1" />

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="@{handler::onSearchClick}"
                android:src="@android:drawable/ic_menu_search" />
        </LinearLayout>

        <fragment
            android:id="@+id/fragment_list"
            android:name="nglauber.android.databinding.BookListFragment"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
    </LinearLayout>
</layout>
Definimos aqui uma variável "handler" que é do tipo BookActivity. Com essa variável podemos definir QUALQUER evento no próprio arquivo de layout. No nosso exemplo definimos que o método onSearchClick da activity (o handler) será chamado quando o botão for clicado utilizando a propriedade android:onClick. Perceba que a separação do objeto com o nome do método é feita por "::".
Vamos ver o código da BookActivity.
public class BookActivity extends AppCompatActivity
    implements BookClickListener {

    ActivityBookBinding mBinding;
    BookListFragment mListFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(
                this, R.layout.activity_book);
        mBinding.setHandler(this);

        mListFragment = (BookListFragment)
                getSupportFragmentManager()
                        .findFragmentById(R.id.fragment_list);
    }

    public void onSearchClick(View view){
        mListFragment.search(
                mBinding.editSearch.getText().toString());
    }

    @Override
    public void onBookClick(Book book) {
        Intent it = new Intent(this, BookDetailActivity.class);
        Parcelable p = Parcels.wrap(book);
        it.putExtra(BookDetailActivity.EXTRA_BOOK, p);
        startActivity(it);
    }
}
No onCreate, utilizamos mais uma vez a classe DataBindingUtil, mas dessa vez chamando o método setContentView. Notem mais uma vez a classe que foi gerada: ActivityBookBinding. Setamos o "handler" do mBinding para podemos tratar o evento de clique que é feito no método onSearchClick (como definimos no arquivo de layout).

Detalhes do Livro

Na tela de detalhes do livro temos oito campos que devem ser exibidos para o usuário. Sem o data binding teríamos que pegar cada referência do componente de UI via findViewById, e atribuir o conteúdo ao componente. Aqui é outro ponto onde o benefício do Data Binding é mais evidente.
Vejamos o arquivo de layout (
<layout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">
  <data>
    <import type="java.util.Arrays" />

    <variable
        name="book"
        type="nglauber.android.databinding.model.Book" />
  </data>

  <ScrollView ... >
    <LinearLayout ... >
      <ImageView ...
        android:id="@+id/img_capa"
        android:src="@{book.imageLinks.thumbnail}" />

      <TextView ...
        android:id="@+id/text_titulo"
        android:text="@{book.title}" />

      <TextView ...
        android:id="@+id/text_titulo"
        android:text="@{book.title}" />

      <TextView ...
        android:id="@+id/text_subtitulo"
        android:text="@{book.subtitle}" />

      <TextView ...
        android:id="@+id/text_autor"
        android:text="@{@string/format_authors(Arrays.toString(book.authors))}" />

      <TextView ...
        android:id="@+id/text_ano"
        android:text="@{@string/format_publish_date(book.publishedDate)}" />

      <TextView ...
        android:id="@+id/text_pages"
        android:text="@{@string/format_pages(book.pageCount)}" />

      <TextView ...
        android:id="@+id/text_description"
        android:text="@{book.description}" />
    </LinearLayout>
  </ScrollView>
</layout>
Todas a propriedades do livro estão sendo atribuídas diretamente no arquivo de layout. A curiosidade aqui fica por conta das strings formatadas que utilizamos para os atributos autores, data de publicação e número de páginas. Essas strings estão no res/values/strings.xml como a seguir.
<string name="format_authors">Autores: %1$s</string>
<string name="format_pages">Páginas: %1$d</string>
<string name="format_publish_date">Data de publicação: %1$s</string>
Vamos agora para o fragment de detalhe BookDetailFragment.
public class BookDetailFragment extends Fragment {
    private static final String EXTRA_BOOK = "livro";

    public static BookDetailFragment newInstance(Book book) {
        BookDetailFragment fragment = new BookDetailFragment();
        Bundle args = new Bundle();
        Parcelable p = Parcels.wrap(book);
        args.putParcelable(EXTRA_BOOK, p);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, 
                             ViewGroup container,
                             Bundle savedInstanceState) {
        Book book = null;
        if (getArguments() != null) {
            Parcelable p = getArguments().getParcelable(EXTRA_BOOK);
            book = Parcels.unwrap(p);
        }
        View view = inflater.inflate(
                R.layout.fragment_detail_book, container, false);

        FragmentDetailBookBinding fdlb = 
                FragmentDetailBookBinding.bind(view);
        fdlb.setBook(book);

        return view;
    }
}
Nada de especial nessa classe que já não vimos anteriormente. Mas já pensou como ela seria sem o data binding? :) 8 findViewById + 7 setText + 1 Image load...
Abaixo temos a aplicação em execução.
Aplicação no Nexus 7

Tela de Listagem no Nexus 5

Tela de detalhes no Nexus 5

Ao terminar de escrever esse artigo só fiquei com uma dúvida: Data Binding, porque você não existe desde a versão 1.5 :) onde você estava???

Mais informações

Palestra sobre Data Binding no Android Dev Summit (em inglês)
https://www.youtube.com/watch?v=NBbeQMOcnZ0
Hangout com o Neto Marin (Google Developer Advocate)
https://www.youtube.com/watch?v=JWpn4yyIJxc
Data Binding Guide (Documentação oficial)
http://developer.android.com/intl/pt-br/tools/data-binding/guide.html

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 4 de abril de 2016

Carregando imagens de outras fontes com o Picasso

Olá pessoal,

As bibliotecas de carregamento de imagens são utilizadas em praticamente todos os projetos Android. Elas facilitam o carregamento de imagens a partir de diversas fontes como: web, sistema de arquivos e até de banco de dados. Possuem recursos de redimensionamento, crop, placeholder, animação, etc. As mais famosas desse segmento são: Picasso, Glide e Universal Image Loader (UIL). Todas são ótimas, já resolvi uns problemas com uma, outros problemas com outra e por aí vai.
No projeto que estou atualmente estou usando o Picasso, e estava precisando carregar imagens de um local não suportado nativamente por ele: o arquivo de expansão de APK.
Graças ao grande mestre Jake Wharton, podemos implementar isso de uma forma muito fácil. Basta criar uma subclasse de RequestHandler.
import com.squareup.picasso.Request;
import com.squareup.picasso.RequestHandler;

public class MeuRequestHandler extends RequestHandler {

    public MeuRequestHandler() {
    }

    @Override
    public boolean canHandleRequest(Request data) {
        // Retorne true se essa classe pode tratar a leitura da imagem
        return data != null
                && data.uri != null
                && data.uri.getScheme() != null
                && data.uri.getScheme().startsWith("ngvl");
    }

    @Override
    public Result load(Request request, int networkPolicy) 
            throws IOException {
        Bitmap imagem = metodoQueCarregaSeuBitmap(request.uri);
        Result result = new Result(imagem, Picasso.LoadedFrom.DISK);
        return result;
    }
}
Essa classe possui apenas dois métodos:
  • canHandleRequest(Request) define se essa classe é capaz de carregar uma determinada imagem. Nesse exemplo, estou usando o parâmetro Request para checar se o endereço (definido por uma Uri) começa com "ngvl". 
  • Já o método load(Request, int) carrega a imagem em si e a retorna por meio de um objeto Result, que recebe o Bitmap da imagem e de onde ela foi carregada (memória, disco ou rede).
Criado o handler, basta adicioná-lo a uma instância do Picasso.
Picasso = mPicassoInstance = 
    new Picasso.Builder(mContext.getApplicationContext())
        .addRequestHandler(new MeuRequestHandler())
        .build();
Com isso, você está adicionando uma nova fonte de imagens à sua instância do Picasso, ou seja, você pode usar todos os schemas já suportados pelo Picasso (http, file, content, ...) e esse que acabamos de criar.
Agora, se invocarmos o código a seguir, o nosso handler tratará essa requisição.
mPicassoInstance
        .load("ngvl://minhaimagem/logo.jpg")
        .into(imageView);

#perfmatters #protip É importante que só haja uma instância desse objeto para evitar problemas de memória! Sendo assim, implemente um Singleton, e caso precise de um Context, passe o getApplicationContext() para ele ;)

4br4ç05,
nglauber

sábado, 19 de março de 2016

Customizando o TextInputLayout com ImageSpan

Olá povo,

 Post curto do dia. Estava querendo customizar o erro do TextInputLayout (TIL) e só consegui graças a ajuda da Lisa Wray, que deu a ótima sugestão de utilizar as classes SpannableStringBuilder + ImageSpan, que eu não conhecia.

 Muitos devs Android, quando vêm algum método que recebe um objeto CharSequence como parâmetro, pensam imediatamente é o mesmo que passar uma String. Mas na verdade o CharSequence é bem mais poderoso, pois permite ir além de textos planos. A classe SpannableStringBuilder é uma subclasse de CharSequence que permite adicionar Spannable's como o ImageSpan.
Vejamos o código a seguir.
TextInputLayout textInputLayout = (TextInputLayout) editText.getParent();
if (campoInvalido()) { // sua verificação do campo vem aqui.
    // Cria a imagem que será incluída no texto do erro.
    // O alinhamento poderia ser ALIGN_BASELINE
    ImageSpan imageSpan = new ImageSpan(
        this, R.drawable.ic_erro, DynamicDrawableSpan.ALIGN_BOTTOM);
    // O SpannableStringBuilder substituirá o primeiro caracter pela imagem
    // por isso o " " no começo.
    SpannableStringBuilder ssbErrorMessage = 
        new SpannableStringBuilder(" "+ getString(R.string.msg_erro));
    // Adicionando a imagem como texto do erro
    ssbErrorMessage.setSpan(imageSpan, 0, 1, 0);

    // Habilita o erro do TIL
    textInputLayout.setErrorEnabled(true);
    // define a mensagem de erro
    textInputLayout.setError(ssbErrorMessage);
    // Atualiza o componente
    textInputLayout.refreshDrawableState();
}
O resultado ficará como a imagem abaixo.
Dois detalhes importantes nesse exemplo:
1) essa abordagem pode ser utilizada por qualquer componentes que receba um CharSequence \o/
2) Podemos utilizar qualquer Bitmap como imagem ;)

 4br4ç05,
nglauber

terça-feira, 15 de março de 2016

ViewPager sem FragmentPagerAdapter + Indicator

Olá povo,

Resolvi escrever esse post porque toda vez que eu preciso disso tenho que procurar na internet. Não que seja difícil de achar, mas me vi na obrigação moral de fazer :)
Quando utilizamos o ViewPager, normalmente estamos fazendo alguma tela de abas ou algo similar. Nesse caso é recomendado fazer com que cada aba/página seja um Fragment. Mas se não houver nenhuma lógica nessas páginas, criar um fragment pode ser desnecessário.
Posso dar como exemplo aquelas telas de boas-vindas que mostram um breve tutorial de como utilizar a aplicação, onde cada passo é uma página com as instruções. Nesse caso, utilizar um layout (definido em um arquivo de layout) para cada página seria o suficiente, e não precisaríamos de Fragment para tal.
Vamos ver como fazer isso,  começando pelo arquivo de layout a seguir.

<LinearLayout
    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:orientation="vertical">

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <com.viewpagerindicator.CirclePageIndicator
        android:id="@+id/indicator"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:paddingBottom="10dp"
        android:paddingTop="10dp"
        android:background="@color/colorPrimary"/>
</LinearLayout>

Esse arquivo possui um ViewPager e um CirclePageIndicator (criado por Jake Wharton) que servirá para exibir um indicador da página atual. Para utiliza-lo, basta adicionar no build.gradle a dependência do componente.
dependencies {
    ...
    compile 'com.android.support:appcompat-v7:23.2.0' 
    compile 'com.github.JakeWharton:ViewPagerIndicator:2.4.1@aar'
}
Vejamos agora o código que cria as páginas do ViewPager.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ViewPager viewPager = (ViewPager)findViewById(R.id.viewpager);
        viewPager.setAdapter(new ViewSimplesAdapter());

        CirclePageIndicator indicator = 
                (CirclePageIndicator)findViewById(R.id.indicator);
        indicator.setViewPager(viewPager);
    }

    private class ViewSimplesAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return 3;
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            int layout;
            if (position == 0){
                layout = R.layout.layout_pagina1;
            } else if (position == 1){
                layout = R.layout.layout_pagina2;
            } else {
                layout = R.layout.layout_pagina3;
            }
            View view = LayoutInflater.from(container.getContext())
                    .inflate(layout, container, false);
            container.addView(view);
            return view;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, 
                                Object object) {
            container.removeView((View)object);
        }
    }
}
A classe ViewSimplesAdapter herda de PagerAdapter e nela, temos o método getCount() que retorna a quantidade páginas que iremos exibir e mais três métodos:
  • isViewFromObject() - basicamente determina se uma View está relacionada com o objeto retornado pelo método instantiateItem().
  • instantiateItem() carrega o arquivo de layout da página específica. Perceba que a View carregada é adicionada ao ViewGroup recebido como parâmetro e é retornada pelo método.
  • destroyItem() vai ser responsável por destruir os itens criados. É importante ressaltar que o ViewPager mantém no máximo três páginas ativas: a que está sendo exibida, a anterior e a posterior.
Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

quinta-feira, 10 de março de 2016

Tchau CESAR :(

Olá povo,

No dia 16 de janeiro de 2006 eu começava a trabalhar no Centro de Estudos e Sistemas Avançados do Recife. Ainda estava terminando a faculdade e lembro que meu sonho era "usar o crachá laranja".
Hoje, 10 de março de 2016 é o meu último dia de trabalho no CESAR. Foram mais de 10 anos de muito desafios, aprendizados, crescimento pessoal e profissional, e muita história boa para contar.
Tive a satisfação de trabalhar com muita gente boa (e outras nem tão legais assim) e fiz muitas amizades em todos os projetos em que passei eu acho.

O nome do CESAR estará cravado na minha história para sempre, com muito orgulho, pois foi nessa empresa fantástica que construí a maior e melhor parte da minha carreira. Isso sem contar a parte acadêmica, já que eu cursei o mestrado no CESAR.edu.

Mas como é evidente a todos que me conhecem, estou fortemente envolvido com a área de desenvolvimento para dispositivos móveis, e nos últimos meses não estava tendo a oportunidade de trabalhar com mobile e nem com desenvolvimento de software (codificação em si). Isso acabou me deixando um pouco desmotivado, então resolvi conversar com meus superiores e verificar a possibilidade de me alocar em um projeto mobile dentro da instituição. Mas infelizmente, fui informado de que não havia previsão de novos projetos com mobile no curto/médio prazo. Sendo assim, chegamos a conclusão de que meu plano de carreira não estava alinhado com as expectativas do CESAR, então resolvi procurar novos desafios em um novo lugar.

Espero de todo coração estar deixando as portas abertas, pois o CESAR é um lugar incrível para trabalhar, que eu considero minha segunda casa, onde (quem sabe?) um dia terei a satisfação de voltar.
Obrigado a todos vocês que fizeram parte da minha vida durante esses 10 anos, vocês me fizeram muito feliz. :)

Chegou a hora de começar uma nova etapa(!) e escrever um novo capítulo da minha história.

4br4ç05,
nglauber

domingo, 28 de fevereiro de 2016

CursorAdapter para RecyclerView (parte 2)

Olá povo,

Na primeira parte desse post, implementamos as classes básicas, conexão com o banco de dados e o ContentProvider. Partiremos agora para a implementação da interface gráfica.

Adapter

Vamos começar a implementação da UI pelo adapter que utilizaremos na lista. Adicione a classe MensagemCursorAdapter e deixe-a como a seguir.
public class MensagemCursorAdapter extends
        RecyclerView.Adapter<MensagemCursorAdapter.VH> {

    private Cursor mCursor;
    private AoClicarNoItem mListener;

    public MensagemCursorAdapter(AoClicarNoItem listener) {
        mListener = listener;
    }

    @Override
    public VH onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_mensagem, parent, false);

        final VH vh = new VH(v);
        v.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = vh.getAdapterPosition();
                mCursor.moveToPosition(position);
                if (mListener != null) mListener.itemFoiClicado(mCursor);
            }
        });

        return vh;
    }

    @Override
    public void onBindViewHolder(VH holder, int position) {
        mCursor.moveToPosition(position);
        int idx_titulo = mCursor.getColumnIndex(MensagemContract.TITULO);
        int idx_descr = mCursor.getColumnIndex(MensagemContract.DESCRICAO);

        String titulo = mCursor.getString(idx_titulo);
        String descricao = mCursor.getString(idx_descr);

        holder.mText1.setText(titulo);
        holder.mText2.setText(descricao);
    }

    @Override
    public int getItemCount() {
        return (mCursor != null) ? mCursor.getCount() : 0;
    }

    @Override
    public long getItemId(int position) {
        if (mCursor != null) {
            if (mCursor.moveToPosition(position)) {
                int idx_id = mCursor.getColumnIndex(MensagemContract._ID);
                return mCursor.getLong(idx_id);
            } else {
                return 0;
            }
        } else {
            return 0;
        }
    }

    public Cursor getCursor(){
        return mCursor;
    }

    public void setCursor(Cursor newCursor){
        mCursor = newCursor;
        notifyDataSetChanged();
    }

    public interface AoClicarNoItem {
        void itemFoiClicado(Cursor cursor);
    }

    public static class VH extends RecyclerView.ViewHolder {
        public TextView mText1;
        public TextView mText2;

        public VH(View v) {
            super(v);
            mText1 = (TextView) v.findViewById(R.id.text1);
            mText2 = (TextView) v.findViewById(R.id.text2);
        }
    }
}
Vou ressaltar apenas os pontos mais importantes do Adapter, pois uma explicação mais introdutória sobre RecyclerView.Adapter foi feita nesse post aqui.
  • Ao invés de termos uma lista de objetos, o Adapter utilizará um cursor para retornar/preencher as views da RecyclerView. Isso pode ser percebido nos métodos onCreateViewHolder() e onBindViewHolder() e no getItemCount().
  • Esse cursor será atribuído por meio do método setCursor(). Utilizaremos esse método no Fragment que mostraremos a seguir. Ao atualizarmos o cursor, chamamos o notifyDatasetChanged() para que a RecyclerView seja atualizada.
  • ATENÇÃO! Não esqueçam de implementar o método getItemId()! Ele é utilizado internamente para atualizar a RecyclerView. Dessa forma, esse método está retornando o ID do registro correspondente.
Como podemos ver, a implementação não é nada complexa comparada ao que fazemos com uma lista de objetos.
A adapter está utilizando o arquivo de layout item_mensagem.xml descrito a seguir.

<android.support.v7.widget.CardView
    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="wrap_content"
    android:layout_marginTop="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp"
    android:orientation="vertical"
    android:foreground="?android:attr/selectableItemBackground"
    app:cardCornerRadius="5dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="8dp">

        <TextView
            android:id="@+id/text1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="20sp"/>

        <TextView
            android:id="@+id/text2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16sp"/>
    </LinearLayout>
</android.support.v7.widget.CardView>
Utilizamos o CardView para dar um visual mais interessante para o nosso modesto aplicativo :)

Um DialogFragment para inserir

Vamos utilizar um Dialog para inserir, mas você pode utilizar o componente que preferir.
Deixe o arquivo de layout conforme a seguir.
<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"
              android:padding="16dp"
              tools:context=".MensagemFragment">

    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/edtTitulo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/titulo"/>
    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/edtDescricao"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/descricao"/>
    </android.support.design.widget.TextInputLayout>

</LinearLayout>
Estamos utilizando o TextinputLayout para dar um toque de Material Design ao nosso aplicativo :) Ele fará uma animação ao preenchermos o conteúdo do EditText. No mais, o arquivo de layout não possui nada de muito especial.
Vamos adicionar no strings.xml as strings utilizadas no layout e as que vamos utilizar no código do Fragment.
<string name="titulo">Título</string>
<string name="descricao">Descrição</string>
<string name="nova_mensagem">Nova mensagem</string>
<string name="editar_mensagem">Editar mensagem</string>
<string name="salvar">Salvar</string>
<string name="cancelar">Cancelar</string>

Vamos agora a implementação da classe MensagemFragment.
public class MensagemFragment extends DialogFragment
        implements DialogInterface.OnClickListener {

    private static final String EXTRA_ID = "id";

    EditText mEdtTitulo;
    EditText mEdtDescricao;
    long id;

    public static MensagemFragment newInstance(long id){
        Bundle bundle = new Bundle();
        bundle.putLong(EXTRA_ID, id);

        MensagemFragment mensagemFragment = new MensagemFragment();
        mensagemFragment.setArguments(bundle);
        return mensagemFragment;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        View view = getActivity().getLayoutInflater()
                .inflate(R.layout.fragment_mensagem, null);

        mEdtTitulo = (EditText)view.findViewById(R.id.edtTitulo);
        mEdtDescricao = (EditText)view.findViewById(R.id.edtDescricao);

        boolean novaMensagem = true;
        if (getArguments() != null && getArguments().getLong(EXTRA_ID) != 0){
            id = getArguments().getLong(EXTRA_ID);
            Uri uri = Uri.withAppendedPath(
                    MensagemContract.URI_MENSAGENS, String.valueOf(id));

            Cursor cursor = getActivity().getContentResolver()
                    .query( uri, null, null, null, null);
            if (cursor.moveToNext()) {
                novaMensagem = false;
                mEdtTitulo.setText(cursor.getString(
                        cursor.getColumnIndex(MensagemContract.TITULO)));
                mEdtDescricao.setText(cursor.getString(
                        cursor.getColumnIndex(MensagemContract.DESCRICAO)));
            }
            cursor.close();
        }

        return new AlertDialog.Builder(getActivity())
                .setTitle(novaMensagem ? 
                        R.string.nova_mensagem : R.string.editar_mensagem)
                .setView(view)
                .setPositiveButton(R.string.salvar, this)
                .setNegativeButton(R.string.cancelar, null)
                .create();
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        ContentValues values = new ContentValues();
        values.put(MensagemContract.TITULO, 
                mEdtTitulo.getText().toString());
        values.put(MensagemContract.DESCRICAO, 
                mEdtDescricao.getText().toString());
        if (id != 0){
            Uri uri = Uri.withAppendedPath(
                    MensagemContract.URI_MENSAGENS, String.valueOf(id));
            getContext().getContentResolver().update(uri, values, null, null);
        } else {
            getContext().getContentResolver().insert(
                    MensagemContract.URI_MENSAGENS, values);
        }
    }
}
A classe é bem simples e será utilizada tanto para inserir quanto para atualizar uma mensagem. Por isso, no método newIntance() estamos recebendo um parâmetro que representa o ID da mensagem. Se passarmos um ID válido, as informações da mensagem serão exibidas na tela. Isso é feito no método onCreateDialog(). Já no método onClick, estamos inserindo/atualizando o registro no banco banco de dados. Perceba que aqui não estou utilizando o AsyncQueryHandler que falei nesse post para simplificar o exemplo, mas ela seria a melhor opção.

Fragment de Listagem

Implementado o adapter, vamos criar o Fragment que utilizará o Adapter para exibir a listagem de mensagens vindas do banco de dados. Vamos começar por seu arquivo de layout.

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ListaMensagensFragment">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"/>

    <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fabAdd"
        android:src="@drawable/ic_add_white_24dp"
        app:fabSize="normal"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp" />
</android.support.design.widget.CoordinatorLayout>
Estamos utilizando o FloatActionButton e o CoordinatorLayout da biblioteca de design.
Partiremos agora para a implementação da classe ListaMensagensFragment.
public class ListaMensagensFragment extends Fragment
        implements LoaderManager.LoaderCallbacks<Cursor< {

    private RecyclerView mRecyclerView;
    private MensagemCursorAdapter mAdapter;
    private LinearLayoutManager mLayoutManager;

    @Override
    public View onCreateView(LayoutInflater inflater, 
                             ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(
            R.layout.fragment_lista_objetos, container, false);

        view.findViewById(R.id.fabAdd).setOnClickListener(
            new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new MensagemFragment()
                        .show(getFragmentManager(), "dialog");
                }
            });

        mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);

        configuraSwipe();

        mLayoutManager = new LinearLayoutManager(getActivity());
        mRecyclerView.setLayoutManager(mLayoutManager);

        mAdapter = new MensagemCursorAdapter(
            new MensagemCursorAdapter.AoClicarNoItem() {
                @Override
                public void itemFoiClicado(Cursor cursor) {
                    long id  = cursor.getLong(
                        cursor.getColumnIndex(MensagemContract._ID));
                    MensagemFragment f = MensagemFragment.newInstance(id);
                    f.show(getFragmentManager(), "dialog");
            }
        });
        mAdapter.setHasStableIds(true);
        mRecyclerView.setAdapter(mAdapter);

        getLoaderManager().initLoader(0, null, this);

        return view;
    }

    private void configuraSwipe() {
        ItemTouchHelper.SimpleCallback simpleItemTouchCallback = 
            new ItemTouchHelper.SimpleCallback(
                0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
            @Override
            public boolean onMove(RecyclerView recyclerView, 
                                  RecyclerView.ViewHolder viewHolder, 
                                  RecyclerView.ViewHolder target) {
                return false;
            }

            @Override
            public void onSwiped(RecyclerView.ViewHolder 
                                 viewHolder, 
                                 int swipeDir) {
                final int x = viewHolder.getLayoutPosition();
                Cursor cursor = mAdapter.getCursor();
                cursor.moveToPosition(x);
                final long id = cursor.getLong(
                        cursor.getColumnIndex(MensagemContract._ID));

                Uri uriToDelete = Uri.withAppendedPath(
                        MensagemContract.URI_MENSAGENS, String.valueOf(id));
                getActivity().getContentResolver().delete(
                        uriToDelete,
                        null, null);
            }
        };
        ItemTouchHelper itemTouchHelper = 
                new ItemTouchHelper(simpleItemTouchCallback);
        itemTouchHelper.attachToRecyclerView(mRecyclerView);
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity(), 
                                MensagemContract.URI_MENSAGENS, 
                                null, null, null, 
                                MensagemContract.TITULO);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        mAdapter.setCursor(data);
    }

    @Override
    public void onLoaderReset(Loader loader) {
        mAdapter.setCursor(null);
    }
}
Vamos as explicações.
  • No método onCreateView(), definimos o evento de clique do FAB onde criamos uma nova instância do MensagemFragment sem passar um ID pois queremos criar uma nova mensagem (e que obviamente não possui um ID).
  • Um detalhe importante aqui é a chamada ao método setHasStableIds() para indicar que cada item da lista possui um identificador único, que no nosso caso é a coluna "_id" da tabela.
  • O método configuraSwipe() é usado para definir o comportamento ao fazermos swipe sobre os itens da lista. No nosso caso, estamos excluindo um registro. Perceba que estamos obtendo a posição do item da lista utilizando o método getLayoutPosition().
  • Voltando ao onCreateView(), estamos criando uma instância do nosso adapter passando como parâmetro um objeto que tratará o evento de clique no item da lista. Note que estamos criando a instância de MensagemFragment passando o ID do item que foi clicado.
  • Após atribuir o adapter à RecyclerView, invocamos o LoaderManager para iniciar o processo de busca no banco de dados de forma assíncrona utilizado o método initLoader(). Quando o Loader estiver criado, o método onCreateLoader será invocado, e nele instanciamos um CursorLoader apontando para URI do provider. Quando a busca no provider é concluída, o método onLoadFinish() é invocado, e onde finalmente atribuímos o Cursor recebido como parâmetro ao nosso adapter.
Precisamos adicionar esse fragment à nossa activity. Abra o arquivo activity_main.xml.
<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context="ngvl.android.exrecyclerview.MainActivity">

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="ngvl.android.exrecyclerview.ListaMensagensFragment"
        android:id="@+id/fragment"/>
</RelativeLayout>

Execute a aplicação e o resultado será similar ao vídeo a seguir.
O código completo da aplicação está disponível no meu github.
https://github.com/nglauber/playground/tree/master/android/ExRecyclerView

4br4ç05,
nglauber

CursorAdapter para RecyclerView (parte 1)

Olá povo,

Recentemente escrevi um post falando sobre como criar uma tela de listagem utilizando o RecyclerView. Mencionei os seus principais benefícios como animações, gestos, gerenciadores de layout, atualização parcial da lista, etc. Para exemplificar esses conceitos, foi criado um adapter para uma lista de objetos. Mas e se quisermos utilizar um Cursor vindo do seu Content Provider?

Já mostrei aqui no blog como utilizar o conjunto ContentProvider + CursorLoader + CursorAdapter. Onde foi utilizado uma ListView para exibir na tela as informações vindas do SQLite. Mas esse conjunto pode ser utilizado com a RecyclerView? Vamos analizar cada um deles...
  • O ContentProvider é uma abstração para um mecanismo de persistência (que normalmente é um SQLite). Como não tem nada a ver com UI, então esse componente funcionará com a RecyclerView. 
  • O CursorLoader nos ajuda a fazer a requisição em background e receber notificações quando alguma operação é realizada no provider. Uma vez que é independente da UI também é compatível. 
  • Já o CursorAdapter não pode ser utilizado com a RecyclerView. Pois ele herda de BaseAdapter e a RecyclerView espera uma instância de RecyclerView.Adapter
Sendo assim, devemos implementar nosso CursorAdapter "na mão", mas relaxe, não é nada complicado... ;) Vamos criar um exemplo completo aqui para entendermos melhor esses conceitos. Crie um novo projeto no Android Studio e vamos lá.

Adicionando dependências

Antes de começarmos a implementação vamos adicionar no build.gradle algumas dependências que utilizaremos no nosso exemplo.
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "ngvl.android.exrecyclerview"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.+'
    compile 'com.android.support:cardview-v7:23.+'
    compile 'com.android.support:recyclerview-v7:23.+'
    compile 'com.android.support:support-v4:23.+'
    compile 'com.android.support:design:23.+'
}

Classes básicas

Vamos adicionar uma interface que possui informações sobre o nosso banco de dados e do ContentProvider.
public interface MensagemContract extends BaseColumns {
    String AUTHORITY = "ngvl.android.exrecycler";
    Uri BASE_URI = Uri.parse("content://"+ AUTHORITY);
    Uri URI_MENSAGENS = Uri.withAppendedPath(BASE_URI, "msgs");

    String TABELA_MENSAGEM = "Mensagem";
    String TITULO = "titulo";
    String DESCRICAO = "descricao";
}
Nossa interface possui uma constante que representa a authority do nosso provider. A authority nada mais é do que o identificador único do nosso provider e faz parte do endereço que utilizaremos para acessá-lo. O endereço completo é representado pela constante URI_MENSAGENS. Em seguida, definimos constantes para o nome da tabela e seus dois campos.
Agora crie a classe DbHelper que será responsável por criar a estrutura do banco de dados da aplicação.
public class DbHelper extends SQLiteOpenHelper {

    public DbHelper(Context context) {
        super(context, "dbNotas", null, 1);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE "+ MensagemContract.TABELA_MENSAGEM +" ("+
                MensagemContract._ID +" INTEGER NOT NULL PRIMARY KEY, " +
                MensagemContract.TITULO +" TEXT NOT NULL, " +
                MensagemContract.DESCRICAO +" TEXT NOT NULL)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}
Quando utilizarmos a classe DbHelper dentro da nossa aplicação, internamente é verificado se banco de dados existe. Caso ele não exista, o método onCreate() será invocado e nele criamos a tabela que armazenará a lista de mensagens.

Content Provider

Adicione agora o ContentProvider da aplicação que se chamará MensagemProvider.
public class MensagemProvider extends ContentProvider {

    public static final int MENSAGENS = 1;
    public static final int MENSAGENS_POR_ID = 2;

    UriMatcher mUriMatcher;
    DbHelper mDbHelper;

    @Override
    public boolean onCreate() {
        mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mUriMatcher.addURI(
            MensagemContract.AUTHORITY, "msgs", MENSAGENS);
        mUriMatcher.addURI(
            MensagemContract.AUTHORITY, "msgs/#", MENSAGENS_POR_ID);

        mDbHelper = new DbHelper(getContext());
        return true;
    }

    @Override
    public String getType(Uri uri) {
        throw new UnsupportedOperationException("Não implementada.");
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        if (mUriMatcher.match(uri) == MENSAGENS){
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            long id = db.insert(
                    MensagemContract.TABELA_MENSAGEM, null, values);
            Uri insertUri = Uri.withAppendedPath(
                    MensagemContract.BASE_URI, String.valueOf(id));
            db.close();
            notifyChanges(uri);
            return insertUri;
        } else {
            throw new UnsupportedOperationException("Não suportada");
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        if (mUriMatcher.match(uri) == MENSAGENS_POR_ID){
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            int linhasAfetadas = db.delete(MensagemContract.TABELA_MENSAGEM,
                    MensagemContract._ID +" = ?",
                    new String[]{ uri.getLastPathSegment() });
            db.close();
            notifyChanges(uri);
            return linhasAfetadas;

        } else {
            throw new UnsupportedOperationException(
                "Uri inválida para exclusão.");
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        if (mUriMatcher.match(uri) == MENSAGENS_POR_ID){
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            int linhasAfetadas = db.update(MensagemContract.TABELA_MENSAGEM,
                    values,
                    MensagemContract._ID +" = ?",
                    new String[]{ uri.getLastPathSegment() });
            db.close();
            notifyChanges(uri);
            return linhasAfetadas;

        } else {
            throw new UnsupportedOperationException(
                "Uri inválida para atualização.");
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        if (mUriMatcher.match(uri) == MENSAGENS){
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            Cursor cursor = db.query(MensagemContract.TABELA_MENSAGEM,
                projection, selection, selectionArgs, null, null, sortOrder);
            cursor.setNotificationUri(getContext().getContentResolver(), uri);
            return cursor;

        } else if (mUriMatcher.match(uri) == MENSAGENS_POR_ID) {
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            Cursor cursor = db.query(MensagemContract.TABELA_MENSAGEM,
                    projection,
                    MensagemContract._ID +" = ?",
                    new String[]{ uri.getLastPathSegment() }, 
                    null, null, sortOrder);
            cursor.setNotificationUri(getContext().getContentResolver(), uri);
            return cursor;

        } else {
            throw new UnsupportedOperationException("Not yet implemented");
        }
    }

    private void notifyChanges(Uri uri){
        if (getContext() != null 
                && getContext().getContentResolver() != null){
            getContext().getContentResolver().notifyChange(uri, null);
        }
    }
}
A primeira coisa que devemos ter em mente é que podemos inserir, alterar, excluir e obter registros em qualquer lugar. Aqui estamos utilizando um banco de dados SQLite, mas nada impede de utilizarmos outras fontes.
Vamos entender um pouquinho do nosso provider:
  • Se acessarmos o provider utilizando qualquer endereço (representado por um Uri) que comece com "content://ngvl.android.exrecycler" ele será chamado. Para restringir esse acesso para apenas alguns endereços específicos utilizamos a classe UriMatcher. Com ela, limitamos o acesso ao provider a apenas dois tipos de endereços:
    • content://ngvl.android.exrecycler/msgs que será utilizado quando não quisermos acessar nenhuma mensagem específica. Utilizaremos esse endereço para inserir uma mensagem e recuperar a lista de mensagens cadastradas. Identificamos esse tipo de endereço como MENSAGENS  com o valor 1.
    • content://ngvl.android.exrecycler/msgs/ID_DA_MENSAGEM será utilizado quando quisermos realizar uma operação em um registro específico. Por exemplo, quando formos excluir ou alterar um registro. Nesse caso, definimos que esse tipo de endereço é MENSAGENS_POR_ID e tem o valor 2.
    • Para cada operação que realizaremos no provider, checamos o tipo da Uri. Se ela não for de nenhum dos dois tipos especificados, levantamos uma exceção.
  • No método insert(), estamos utilizando nossa classe DbHelper para obter a instância do banco e fazer a inclusão no banco de dados. O detalhe importante aqui é que após inserir o registro, estamos invocando o método notifyChange().
    Ele é crucial, pois avisará que o provider representado pela Uri recebida como parâmetro sofreu modificações. No nosso exemplo, quando isso acontecer, a lista de mensagens que implementaremos mais adiante será atualizada automaticamente.
  • Os métodos delete e update seguem a mesma lógica do insert. Eles excluem e atualizam um determinado registro e avisam que isso aconteceu invocando o método notifyChange().
  • A outra parte da mágica é feita no método query(). Quando realizamos uma busca no banco de dados, um objeto cursor é retornado e nele, atribuímos uma Uri. Quando essa Uri sofre alguma modificação, esse cursor é notificado e atualizado com as novas informações. E quem está notificando esse cursor? Nosso método notifyChange() ;)
    Outra curiosidade no método query() é que podemos obter uma mensagem utilizando a Uri content://ngvl.android.exrecycler/msgs/ID_DA_MENSAGEM.
Certifique-se de que o Content Provider está declarado no AndroidManifest.xml como a seguir:
<application ...>
    <provider
        android:name=".MensagemProvider"
        android:authorities="ngvl.android.exrecycler"
        android:enabled="true"
        android:exported="true">
    </provider>
Criadas as classes básicas e o mecanismo de persistência no banco, vamos começar a implementação da interface gráfica do nosso exemplo que implementaremos no próximo post.

4br4ç05,
nglauber