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

Minha palestra sobre Data Binding
http://www.nglauber.com.br/2016/06/androidos-2016.html
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

18 comentários:

Alex Leiva disse...

Parabéns pelo seu artigo Glauber!
Muito se fala sobre o assunto, mas muito pouco se ensina.
Obrigado pela sua preocupação em nos manter informados e pela boa didática.
Continue assim!

Alex Leiva disse...

Olá Glauber!
Gostaria de saber se seria possível você nos ajudar a construir um cadastro simples usando o databinding. Pode ser um do tipo agenda de contatos; só para termos uma ideia de como podemos usar este recurso para o Android.
Desde já agradeço a sua atenção.

Grande abraço.

Nelson Glauber disse...

Oi Alex,

Que bom que gostou do post!

Posso tentar fazer esse tutorial, mas semana que vem vou estar cheio de novidades do Google I/O. Então eu não garanto :)

Você pode tentar adaptar esses dois posts abaixo onde eu faço um "CRUD" para usar Data Binding. ;)
http://www.nglauber.com.br/2016/02/cursoradapter-para-recyclerview-parte-1.html
http://www.nglauber.com.br/2016/02/cursoradapter-para-recyclerview-parte-2.html

Fica o desafio! ;)

4br4ç05,
nglauber

Hugo Landim disse...

Eu posso usar a tag sem o a tag ?, apenas para dar o Binding das minhas views e poder manipular elas sem precisar dar o findViewById, eu tentei é a classe MeuFragmentBinding.java não foi gerada pela API :/

Nelson Glauber disse...

Oi Hugo,

Acho que o blogger "comeu" as tags que você colocou. Então não entendi bem a pergunta.
Mas a classe do binding é pra ser gerada automaticamente se o data binding estiver habilitado.

4br4ç05,
nglauber

Hugo Landim disse...

Oi Glauber,
Minha dúvida era se eu consigo dar um Bind sem model, apenas da tela(fragment/activity), mas dando uma olhada melhor no posto vi que posso passar minha Activity como variável (Sensacional)!!

Mas agora tenho outra dúvida, quando eu uso a tag include dentro do meu xml, a classe MeuXmlBinding não gera os parâmetros das views que estão dentro xml utilizado na tag include eu precisei adicionar um id a tag include fazer algo parecido a meuXmlBinding.idDaTagInclude.findViewById(R.id.idViewDentroDoInclude);
Essa é a melhor forma?

Nelson Glauber disse...

Oi Hugo,

Você pode usar com o include sim. Dá uma olhada na documentação:
https://developer.android.com/topic/libraries/data-binding/index.html#includes

Basicamente você seta sua variável na tag include usando bind:suaVariavel.

4br4ç05,
nglauber

Hugo Landim disse...

Show!! Funcionou perfeitamente!!
Parabéns pelo post Mestre!

Levi Saturnino disse...

Olá Glauber, ajudou bastante a tirar as minhas dúvidas.

Tito Albino E. da Silva Junior disse...

Primeiramente parabéns pelo post, implementando o Data Binding Library em meu projeto não tive problema com telas de detail por exemplo mas já no meu RecyclerView.Adapter sim, conseguir implementar ta fazendo a ligação com o layout mas observei que a renderização não esta correta fica alterando as informações de alguns itens em tempo de execução? sabe o que pode ser, alguém passou por isso?

Nelson Glauber disse...

Oi Tito,

Você está chamando o holder.binding.executePendingBindings() ?
Esse método serve justamente para evitar esses problemas.

4br4ç05,
nglauber

Tito Albino E. da Silva Junior disse...

Sim, estou chamando o executePendingBindings() , segue o código que estou utilizando https://gist.github.com/titoaesj/849aa3a19518a244d1a62a4e95a6c16b

Nelson Glauber disse...

Oi Tito,

Você usando um método membros estáticos

Ex.: private static TaskItemBinding mBinding;
public static void bindTo(Task task) { ... }

Casa linha é uma instância do ViewHolder, logo, o Binding sendo estático afetará todas as instâncias.
Faz baseado no exemplo, que vai ficar lindo ;)

4br4ç05,
nglauber

Tito Albino E. da Silva Junior disse...

Muito obrigado mestre Nelson Glauber, consegui aqui o meu ViewHolder estava errado, já fiz até @BindingAdapter rs, agora fazer o #databinding no restante do projeto.

Luciano Marques disse...

Esse trecho de código não funciona: @={user.age} Se age for do tipo int dá erro.
Parece que o two-way do data binding não funciona como tipos int ou double. Alguém sabe como resolver esse problema?

Nelson Glauber disse...

Oi Luciano,

Não sei de que trecho você está falando. Não uso @={user.age} em nenhum lugar nesse exemplo.
Mas respondendo sua pergunta, você deve usar um BinderAdapter para resolver esse problema. Eu falo dele na minha apresentação sobre DataBinding.
http://www.nglauber.com.br/2016/06/androidos-2016.html
Ou você pode usar a documentação.
https://developer.android.com/topic/libraries/data-binding/index.html

4br4ç05,
nglauber

Dáulio Oliveira disse...

Parabéns pelo post.
Glauber, estou com um problema na criação de um projeto com databinding. Ocorre um erro em tempo de compilação que não encontra o pacote onde está a Classe Binding.
Erro: package com.apps.android.menuapp.databinding does not exist

Você sabe o que pode ser?

Abraço

Nelson Glauber disse...

Oi Dáulio,

Você habilitou o Data Binding no build.gradle? Caso contrário, o problema é em algum dos seus arquivos de layout, o que impede que as classes do DataBinding sejam geradas.

4br4ç05,
nglauber