terça-feira, 12 de julho de 2016

Minhas Aulas de Android no YouTube

Olá pessoal,

Quando estava preparando minhas aulas de iOS (isso mesmo, já ministrei aulas de iOS ;) procurei por uma boa referência atualizada sobre o assunto e achei as aulas da Universidade de Stanford disponíveis no iTunes. Ótimas por sinal.
Seguindo esse exemplo, resolvi também gravar minhas aulas de Android na Unibratec e disponibiliza-las no YouTube. A gravação não tem nenhuma edição, é apenas meu desktop com o áudio no microfone embutido do computador. São 19 vídeos com mais de 20h de conteúdo.
As aulas mostram desde os conceitos mais básicos, com a construção de pequenos aplicativos de exemplo, até o desenvolvimento de um projeto simples, mas que possui: uma UI diferenciada para smartphones e tablets; acesso a um servidor web para realizar a leitura de um arquivo JSON; utilização de algumas bibliotecas famosas do Android como Butter Knife, Otto, OkHttp, etc.; e persistência de dados no banco de dados SQLite.


Espero que gostem!

4br4ç05,
nglauber

segunda-feira, 20 de junho de 2016

Androidos 2016

Olá pessoal,

No próximo dia 02 de julho, na Unipê em João Pessoa - PB, participarei pelo segundo ano consecutivo do Androidos! Um dos melhores eventos do Nordeste voltados para desenvolvedores Android e abordará temas muito legais do desenvolvimento de aplicações Android tais como: arquitetura, programação reativa, serviços de cloud, e muito mais.
Este ano, o Androidos contará com a participação de três Google Developer Experts em Android do Brasil: Marcelo Quinta (de Goiás), Ubiratan Soares (de São Paulo) e eu ;) Isso sem falar na ilustre participação do Josias Paes (Unipê) e do Gustavo Soares (IFPB).


Farei duas participações durante o evento: a primeira com uma palestra sobre Data Binding e log em seguida participarei de um Fireside chat, onde o público poderá fazer perguntas e tirar suas próprias dúvidas sobre Android.

Se você é desenvolvedor Android, não poder perder essa oportunidade. Mais informações no site do evento. Nos vemos lá!

[EDITADO 08/07/2016]
Slides da minha palestra.


O código-fonte do projeto mostrado nos slides está no meu GitHub.
https://github.com/nglauber/playground/tree/master/android/LivrosFirebase

4br4ç05,
nglauber

sexta-feira, 17 de junho de 2016

Firebase com Android

Olá povo,

Estava com vontade de escrever uns posts sobre Firebase aqui para o blog, mas achei que eles ficariam muito grandes. Então resolvi gravar alguns vídeos, com praticamente nenhuma edição, exceto alguns cortes que eu fiz pela demora na compilação/execução da aplicação.

É uma série com quatro vídeos onde abordo três dos principais serviços do Firebase: autenticação (Auth), armazenamento de arquivos (Storage) e o banco de dados em tempo real (RealTime database).

A ideia aqui foi criar um cadastro de livros do zero, onde o usuário deve realizar o login utilizando sua conta do Google para acessar a aplicação. O cadastro permite digitar o título do livro, o autor, e fazer o upload da capara do livro, obtida tirando uma foto com a câmera do aparelho. Para persistir as informações utilizamos o banco de dados do firebase, e para armazenar as imagens das capas do livro usamos o serviço de storage. O resultado ficou como abaixo:

     

Como vocês podem/vão notar, não me preocupei muito com a UI. Mas isso é uma melhoria que posso fazer nos próximos vídeos. Se vocês gostarem, posso evoluir esse aplicativo e/ou mostrando mais recursos do Firebase (como o analytics, crash report, login com facebook, etc).
Então, deixem seus comentários e espero que gostem! ;)



[EDITADO 08/07/2016]
O código desse exemplo (melhorado) encontra-se no meu GitHub:
https://github.com/nglauber/playground/tree/master/android/LivrosFirebase

4br4ç05,
nglauber

segunda-feira, 13 de junho de 2016

Chrome Custom Tabs para Android

Olá povo,

Mais um post rápido :) Quando precisamos abrir uma URL dentro de uma aplicação Android normalmente utilizamos uma Intent como a seguir:
Intent it = new Intent(Intent.ACTION_VIEW, 
                       Uri.parse("http://www.nglauber.com.br"));
startActivity(it);
O problema de usar essa abordagem é que exige o carregamento de outro aplicativo (o que é pesado computacionalmente falando) e fará com que o usuário deixe seu aplicativo, o que não é bom. Outro ponto negativo é que uma vez que você não tem acesso ao outro aplicativo (o browser nesse caso), você não consegue fazer nenhuma customização.

Uma outra abordagem é utilizar o componente WebView.
WebView webView = (WebView)findViewById(R.id.webView);
webView.loadUrl("http://www.nglauber.com.br");
webView.setWebViewClient(new WebViewClient() {
    public boolean shouldOverrideUrlLoading(WebView view, String url){
        view.loadUrl(url);
        return false; 
    }
});
Nesse caso você possui um controle maior, uma vez que tudo é feito dentro do seu aplicativo. Entretanto, você terá que fazer vários controles para a navegação (como eu fiz aqui tratando o redirecionamento de página). Um problema aqui é que o WebView não compartilha a sessão do browser.

Chrome Custom Tabs junta os pontos positivos de ambas as abordagens, pois permite o controle da experiência web dentro do seu aplicativo. Com essa API podemos: definir transições customizadas; personalizar título; adicionar ações personalizadas; fazer um pré-carregamento da página para agilizar o carregamento; entre outros.
Como pode-se imaginar, esse recurso só funcionará se o usuário possuir o Chrome instalado no aparelho. Caso contrário, será disparada a Intent que mostramos no início do post.
 Para usar o Chrome Custom Tabs, a primeira coisa a fazer é adicionar a dependência no build.gradle.
dependencies {
    ...
    compile 'com.android.support:customtabs:23.3.0'
}
Então é só chamar a aba como a seguir:
String url = "http://www.nglauber.com.br";
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();

// Definindo a cor da toolbar (opcional)
builder.setToolbarColor(ActivityCompat.getColor(this, R.color.colorPrimary));

// Adicionando uma ação/menu (opcional)
Bitmap icon = BitmapFactory.decodeResource(
        getResources(), android.R.drawable.ic_menu_agenda);
PendingIntent pit = PendingIntent.getActivity(
        this, 0, new Intent(this, MainActivity.class), 0);

// Ação
builder.setActionButton(icon, "Ação", pit, true);
// Menu
builder.addMenuItem("Menu", pit);

// Definindo animações (opcional)
builder.setStartAnimations(this, 
        R.anim.slide_in_right, R.anim.slide_out_left);
builder.setExitAnimations(this, 
        R.anim.slide_in_left, R.anim.slide_out_right);

// Abrindo a "aba"
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl(this, Uri.parse(url));

Como podemos observar, é bastante simples criar as custom tabs do Chrome. Criamos uma instância da classe CustomTabsIntent.Builder e com ela criamos podemos customizar a aba da maneira que desejarmos. Primeiro definimos a cor do título usando o método setToolbarColor(int). Em seguida instancimamos um Bitmap e uma PendingIntent que podem ser utilizadas para adicionar uma ação ou uma opção de menu na barra de título. A ação é adicionada por meio do método setActionButton(Bitmap,String,PendingIntent,boolean) e o menu por meio do método addMenuItem(String,PendingIntent).
As animações foram definidas utilizando o método setStartAnimations e setExitAnimations. Essas animações foram definidas como a seguir:
// res/anim/slide_in_left.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

// res/anim/slide_in_right.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

// res/anim/slide_out_left.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="-100%p"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

// res/anim/slide_out_right.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="100%p"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>
Por fim, criamos uma instância da classe CustomTabsIntent e com o método launchUrl(Activity, Uri) abrimos essa nova aba.
Abaixo podemos ver a aplicação em execução.

Mais detalhes aqui:
https://developer.chrome.com/multidevice/android/customtabs

4br4ç05,
nglauber

terça-feira, 7 de junho de 2016

Carregando imagens da galeria do Android

Olá povo,

Nesse post rápido vou mostrar como carregar imagens da galeria de mídia do Android. O único detalhe a observar nesse post é que estamos carregando imagens tanto locais quando as que estão em algum serviço da nuvem como DropBox e Google Photos. Por isso, para começar coloque as permissões necessárias no AndroidManifest.xml.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
Lembrando que para o Android 6 (API Level 23) você deve checar essas permissões em tempo de execução.

Em seguida, inicie uma nova activity utilizando a ação ACTION_GET_CONTENT.
private static final int IMAGE_GALLERY_REQUEST = 1;
...
public void selectImageClick(View view) {
    if (ActivityCompat.checkSelfPermission(this, 
            Manifest.permission.READ_EXTERNAL_STORAGE) ==
            PackageManager.PERMISSION_GRANTED) {
        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_GET_CONTENT);
        startActivityForResult(
                Intent.createChooser(
                        intent,
                        getString(R.string.select_picture_title)),
                IMAGE_GALLERY_REQUEST);
    } else {
        ActivityCompat.requestPermissions(this,
                new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE },
                IMAGE_GALLERY_REQUEST);
    }
}
Ao selecionarmos uma imagem, o método onActivityResult será chamado.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == IMAGE_GALLERY_REQUEST && resultCode == RESULT_OK){
        new LoadImageTask(this).execute(data.getData());
    }
}

Como falamos anteriormente, carregaremos as imagens do sistema de arquivos ou da web. Por isso, devemos realizar essa operação fora da UI thread, então definimos a classe LoadImageTask.
class LoadImageTask extends AsyncTask<Uri, Void, Bitmap> {

    WeakReference<PickImageActivity> mActivity;

    public LoadImageTask(PickImageActivity activity) {
        this.mActivity = new WeakReference<>(activity);
    }

    public PickImageActivity getActivity() {
        return mActivity.get();
    }

    @Override
    protected Bitmap doInBackground(Uri... params) {
        if (getActivity() != null) {
            try {
                return BitmapFactory.decodeStream(
                        getActivity().getContentResolver().openInputStream(params[0]));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        if (getActivity() != null){
            getActivity().showImage(bitmap);
        }
    }
}
Temos algumas coisas interessantes aqui. A primeira é essa classe não deve ser uma inner class da Activity/Fragment, pois uma inner class sempre contém uma referência para sua outer class. Uma vez que ela não é uma inner class, devemos passar a referência da activity/fragment para essa classe. Estamos utilizando uma WeakReference para armazenar a instância da activity, o motivo disso é evitar o leak de memória, já que se o usuário sair da tela antes da imagem ser baixada aquela memória ficará alocada desnecessariamente durante algum tempo.
Outra coisa interessante é que estamos utilizando o método openInputStream() da classe ContentResolver para obter a imagem propriamente dita.
A imagem retornada terá o seu tamanho original, se você preferir, pode utilizar a técnica de redimensionamento de imagem que eu mostrei nesse post.
Por fim, no onPostExecute, invocamos o método showImage da Activity.
public void showImage(Bitmap bitmap) {
    ImageView imageView = (ImageView)findViewById(R.id.imageView);
    if (imageView != null){
        imageView.setImageBitmap(bitmap);
    }
}
Qualquer dúvida, deixe seu comentário ;)

 4br4ç05,
nglauber

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

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