Data Binding Para Vinculo de Dados na UI Android

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /Data Binding Para Vinculo de Dados na UI Android

Data Binding Para Vinculo de Dados na UI Android

Vinícius Thiengo
(8722) (6)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloAprenda Domain-driven Design: Alinhando Arquitetura de Software e Estratégia de Negócios
CategoriaEngenharia de Software
Autor(es)Vlad Khononov
EditoraAlta Books
Edição1ª
Ano2024
Páginas320
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Tudo bem?

Neste artigo vamos ao estudo e aplicação da biblioteca Android Data Binding, popular biblioteca que ajuda a diminuir o código boilerplate de vinculo de dados e métodos à interface do usuário.

Na primeira parte do artigo vamos ao estudo da library, na segunda parte vamos a aplicação dela em um projeto Android para venda de tênis:

Animação app de tênis com a Data Binding API

Neste artigo o termo "API" será utilizado como sinônimo de "biblioteca" (library).

Antes de prosseguir, não esqueça de se inscrever 📩 na lista de emails do Blog para ter acesso aos conteúdos exclusivos.

A seguir os tópicos que estaremos estudando:

Android Data Binding

O termo Data Binding já é bem conhecido no desenvolvimento de software. No Android trabalhamos com o binding de dados com frequência, mesmo que não de maneira eficiente.

O simples código a seguir, em Kotlin, é um exemplo de binding de dados e método:

...
textview_name_user = user.name

button_test.setOnClickListener {
view -> Toast
.makeText( this, "Test", Toast.LENGTH_SHORT )
.show()
}
...

 

A principal proposta da biblioteca Android Data Binding é diminuir, ou remover por completo, todo esse código boilerplate de acesso a Views e então vinculação de dados e métodos a elas.

O vinculo ainda vai existir, porém em código estático, direto no layout XML. Com isso é possível conseguir maior tempo e espaço em código para foco somente no domínio de problema do aplicativo em desenvolvimento.

Devido aos benefícios alcançados com o uso de Data Binding no desenvolvimento Android, muitas empresas de desenvolvimento mobile exigem do profissional o conhecimento desta API.

A documentação é bem completa, há até uma parte em português, "Visão Geral". Neste artigo vou abordar as partes que identifiquei como mais relevantes, logo, essa não é uma fonte única de estudo e recomendo também que você leia a documentação oficial para estudar alguns conceitos mais avançados que poderão ser úteis a ti: Documentação oficial Data Binding Android.

Assim podemos partir para os códigos. Não deixe de também acompanhar o projeto de exemplo que é a segunda parte deste artigo, nele ficará mais evidente os ganhos quando utilizando a Android Data Binding API.

Instalação da biblioteca

Diferente de quando utilizando o Java, com o Kotlin a configuração de instalação da biblioteca Data Binding exige algumas referências a mais, mas todas no Gradle App Level, ou build.gradle (Module: app):

...
/*
* Plugin que permite o trabalho com anotações e
* com a biblioteca Data Binding no Kotlin.
* */
apply plugin: 'kotlin-kapt'

android {
...
/*
* Liberando o trabalho com Data Binding
* */
dataBinding{
enabled = true
}
}

dependencies {
...
/*
* Inclusão do pacote da Data Binding library.
* */
kapt 'com.android.databinding:compiler:3.1.4'
}

 

Até o momento da construção deste conteúdo a versão estável mais atual da com.android.databinding era a 3.1.4.

A versão da biblioteca é a mesma versão do plugin do Gradle, logo, no Gradle Project Level, ou build.gradle (Project: NomeApp), podemos ter:

buildscript {
ext.android_plugin_version = '3.1.4'
...
dependencies {
classpath "com.android.tools.build:gradle:$android_plugin_version"
...
}
}

 

E então no Gradle App Level teríamos a atualização da referência 'com.android.databinding:compiler':

...
kapt "com.android.databinding:compiler:$android_plugin_version"
...

 

Para acompanhar as versões da API, entre em MVN Repository Data Binding. Escolha sempre utilizar a versão estável mais atual para seus projetos em produção.

Antes de partirmos para a próxima seção, saiba que se você estivesse utilizando a linguagem Java somente o código a seguir, no Gradle App Level, é que seria necessário para liberar o trabalho com a Data Binding API:

...
android {
...
/*
* Liberando o trabalho com Data Binding
* */
dataBinding{
enabled = true
}
}
...

Códigos estático e dinâmico sem Data Binding

Para seguirmos os estudos da Data Binding de maneira eficiente, primeiro vamos ao XML de layout sem a sintaxe desta API:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp" />

<TextView
android:id="@+id/tv_last_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp" />
</LinearLayout>

 

Agora a simples classe de domínio, User:

open class User(
val firstName: String,
val lastName: String,
val age: Int = 20,
val isAdult: Boolean = true )

 

Então o código de vinculo de alguns dados de user ao layout apresentado:

...
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_main )

val user = User( "Thiengo", "Blog" )

tv_first_name.text = user.firstName
tv_last_name.text = user.lastName
}
...

 

Observação: aqui estamos assumindo que o plugin kotlin-android-extensions está sendo utilizado e assim não precisamos de códigos de acesso com findViewById().

Executando o projeto Android com os códigos apresentados, temos:

Dados user vinculados a layout sem Data Binding

Trabalhando o binding de dados

O binding direto em arquivo XML de layout é bem simples. Vamos a atualização do último XML apresentado, já fazendo uso da Data Binding API:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<variable
name="user"
type="thiengo.com.br.databindingtests.domain.User" />
</data>

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

<TextView
android:id="@+id/tv_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{ user.firstName }" />

<TextView
android:id="@+id/tv_last_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{ user.lastName }" />
</LinearLayout>
</layout>

 

A tag <layout> passa a ser a tag root do layout. O bloco <data> é onde estarão as variáveis, <variable>, e as importações, <import>. E então as referências via lambda @{ ... }. A expressão dentro de @{} deve retornar o valor esperado pelo atributo.

Em <variable> temos os atributos:

  • name: contém o rótulo da variável, o mesmo que será utilizado no arquivo XML;
  • type: contém o tipo da variável.

Ainda é preciso o binding no código dinâmico. Logo, no mesmo onCreate() apresentado anteriormente, temos agora:

...
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )

/*
* Busque colocar o código de acesso a classe binding
* gerada, aqui a classe ActivityMainBinding, exatamente
* aonde estava o código de vinculo de layout,
* setContentView(R.layout.activity_main), pois assim é
* possível manter códigos de acesso a Views que ainda
* não foram (ou não serão) atualizados para a sintaxe
* Data Binding em XML.
* */
val binding = DataBindingUtil
.setContentView<ActivityMainBinding>( this, R.layout.activity_main )

val user = User("Thiengo", "Blog")

/*
* Vinculando o objeto à variável referente a ele no
* XML de layout. É comum utilizar os mesmos rótulos,
* facilita a leitura do projeto.
* */
binding.user = user
}
...

 

O código DataBindingUtil.setContentView() é um possível código de vinculo de layout ao objeto dele (atividade, fragmento, dialog, ...). Na verdade o DataBindingUtil.setContentView() é utilizado somente em contextos de atividades.

O primeiro argumento é a atividade host do layout e o segundo argumento é a referência do layout.

Note que para cada layout com código Data Binding é gerada uma classe binding, classe tendo como rótulo o nome do arquivo de layout em modelo CamelCase e com o sufixo Binding. Aqui a classe binding gerada foi a ActivityMainBinding, referente ao layout activity_main.xml.

As variáveis definidas em <data> estarão presentes como propriedades do objeto da classe binding. E é preciso o vinculo como foi realizado em binding.user = user.

Se o vinculo não ocorrer, as Views não serão preenchidas e não haverá exceção devido a isso.

Executando o código anterior, temos o mesmo resultado:

Dados user vinculados a layout com Data Binding

Se removermos os comentários da última atualização de onCreate() teremos um código ainda menor do que o anterior. Isso neste exemplo simples.

Problemas com a geração da classe binding

Caso você tenha já verificado todo o seu código Data Binding, certificado de que ele está correto e mesmo assim alguma classe binding ainda não tenha sido gerada.

Como possíveis soluções, temos:

  • Aplicando o Clean e Rebuild:
    • Acesse "Build" no menu de topo do Android Studio;
    • Acione "Clean Project";
    • Ao final do processamento de "Clean Project", volte em "Build";
    • Agora acione "Rebuild Project";
    • Ao final do processamento de "Rebuild Project", verifique se a classe binding foi gerada.
  • Reiniciando o Android Studio com limpeza de cache:
    • Acesse a opção "File" no menu de topo do IDE;
    • Acione a opção "Invalidate Caches / Restart...";
    • Na caixa de diálogo aberta acione "Invalidate and Restart";
    • Ao final da reinicialização do IDE, verifique se a classe binding foi gerada.

Alguns desenvolvedores Android informaram na comunidade que a adição de generateStubs = true ao Gradle App Level, como a seguir, fez com que as classes binding problemáticas fossem geradas:

...
dependencies {
...
}

kapt {
generateStubs = true
}
...

 

Logo, se mesmo tentando as duas primeiras soluções indicadas a classe binding não for gerada, tente a adição do código em destaque acima e sincronize o projeto.

Importando entidades

Na sintaxe XML Data Binding também é possível importar entidades por meio da tag <import>. Veja o código a seguir:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>

<import type="thiengo.com.br.databindingtests.domain.User" />

<variable
name="user"
type="User" />
</data>
...
</layout>

 

O type de <variable> pôde referenciar somente o nome principal do tipo, neste caso. Tipos de dados que já estão presentes no pacote java.lang não precisam da adição explícita por meio de <import>. A classe String é um desses tipos.

Quando você passar a utilizar mais a biblioteca Data Binding notará que muitas vezes você estará utilizando <import> para aquelas classes estáticas de seu domínio de problema que contém constantes e métodos úteis às visualizações do layout.

Invocando métodos

Também é possível trabalharmos a invocação de métodos. Veja a seguir a atualização da classe User:

open class User(
val firstName: String,
val lastName: String,
val age: Int = 20,
val isAdult: Boolean = true ) {

fun fullName(): String =
String.format( "%s %s", firstName, lastName )
}

 

E então a adição de mais uma View no layout, View que recebe valor de fullName():

...
<LinearLayout ...>
...

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{ user.fullName() }" />
</LinearLayout>
...

 

Executando o projeto com o algoritmo atualizado, temos:

Data binding com vinculo de método

Também é possível passar parâmetros e colocar métodos de outras classes. A seguir uma nova classe, BindListener:

class BindListener {

fun onClickName( view: View, firstName: String, lastName: String ){
Toast
.makeText( view.context, "$firstName $lastName", Toast.LENGTH_SHORT )
.show()
}
}

 

Então a adição de uma variável BindListener ao layout:

...
<data>
...

<variable
name="bindListener"
type="thiengo.com.br.databindingtests.domain.BindListener" />
</data>
...

 

Agora o vinculo de um objeto BindListener a classe binding do layout:

...
binding.bindListener = BindListener()
...

 

Importante: é possível que a nova variável adicionada ao layout não apareça como opção de propriedade do objeto da classe binding, neste caso você deve dar o rebuild no projeto, "Menu de topo" > "Build" > "Rebuild Project".

Por fim, voltando ao layout, o código de invocação do método onClickName():

...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:onClick="@{ (view)->bindListener.onClickName( view, user.firstName, user.lastName ) }"
android:text="@{ user.fullName() }" />
...

 

Como informado nas seções iniciais, @{} representa um lambda, com isso, quando não omitidos os parâmetros devem vir à esquerda da seta (->) e a expressão à direita.

Ok, mas como eu sei quais são os possíveis parâmetros?

No caso da invocação de método é possível saber os parâmetros de acordo com o atributo em uso.

No exemplo anterior estamos utilizando o android:onClick, e como sabemos, pelo uso de onClick() da Interface View.OnClickListener, o único argumento enviado é a View que tem o vinculo com o método ouvidor.

Executando o projeto com o novo código e acionando o TextView de nome completo, temos:

Listener de clique via Data Binding

Veja como seria no caso de um CheckBox, por exemplo. Na classe BindListener adicione o método a seguir:

...
fun mudancaStatus( checkBox: CompoundButton, status: Boolean ) {

Toast
.makeText( checkBox.context, "Novo status: $status", Toast.LENGTH_SHORT )
.show()
}
...

 

Agora a atualização no layout:

...
<CheckBox
android:text="Apenas um teste com o CheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked)->bindListener.mudancaStatus( cb, isChecked )}" />
...

 

Se você acessar a documentação de CompoundButton.OnCheckedChangeListener confirmará que o onCheckedChanged() tem dois parâmetros.

Executando o projeto com o novo código e mudando o valor do CheckBox, temos: 

Listener de status de CheckBox via Data Binding

Configurando métodos ouvidores

É possível trabalhar diretamente com métodos ouvidores de Interfaces criadas somente para isso. Veja a seguir o novo código de BindListener:

class BindListener: View.OnLongClickListener {
...

override fun onLongClick( view: View ): Boolean {
Toast
.makeText( view.context, "Clique longo realizado", Toast.LENGTH_SHORT )
.show()

/*
* Retorne true se o callback consumiu o clique longo,
* retorne false caso contrário.
* */
return true
}
}

 

Então a vinculação do método ouvidor direto no atributo android:onLongClick:

...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:onClick="@{ (view)->bindListener.onClickName( view, user.firstName, user.lastName ) }"
android:onLongClick="@{ bindListener::onLongClick }"
android:text="@{ user.fullName() }" />
...

 

Executando o projeto com o novo código e acionando o toque / clique longo no TextView de nome completo, temos:

Listener de clique longo via Data Binding

Note que a sintaxe variável::método pode ser utilizada para qualquer método que respeite a assinatura e retorno de método aguardado pelo atributo em uso. No caso do android:onLongClick poderia ser o método a seguir, que não vem de nenhuma Interface exclusiva de listener:

class BindListener: ... {
...
fun newOnLongClick( view: View ): Boolean {
Toast
.makeText( view.context, "Clique longo realizado - newOnLongClick()", Toast.LENGTH_SHORT )
.show()

/*
* Retorne true se o callback consumiu o clique longo,
* retorne false caso contrário.
* */
return true
}
}

 

A documentação informa que a possibilidade de conflito entre gerenciadores de eventos que também fariam uso do evento de clique já foi resolvida com a adição de novos atributos:

ClasseConfigurador do ouvinteAtributo
SearchViewsetOnSearchClickListener( View.OnClickListener ) android:onSearchClick
ZoomControlssetOnZoomInClickListener( View.OnClickListener )android:onZoomIn
ZoomControlssetOnZoomOutClickListener( View.OnClickListener )android:onZoomOut

Ouvidores via @BindingAdapter

É possível colocar listener de atributo, até mesmo de atributo personalizado. A anotação @BindingAdapter nos permiti isso.

Para esta seção, assuma que a Picasso API já está incluída no projeto e a permissão de Internet adicionada ao AndroidManifest.xml.

Antes de apresentar um método BindAdapter saiba que ele tem de ser estático e no Kotlin não basta somente colocarmos o método no escopo de um companion object, ainda é necessário o uso da anotação @JvmStatic

Na classe BindListener adicione o trecho de código a seguir:

...
companion object {

@JvmStatic
@BindingAdapter( "picassoLoad" )
fun loadRemoteImage( iv: ImageView, url: String ){

Picasso.get().load( url ).into( iv )
}
}
...

 

Dê atenção especial ao código acima, pois na época da construção deste conteúdo, na documentação oficial, quando apresentando os códigos em versão Kotlin, não havia nada sobre a sintaxe necessária de "entidade estática" para que métodos BindAdapter funcionassem no Kotlin, alias tinha um código não atualizado que simplesmente nem permitia a compilação do projeto.

Voltando a anotação @BindingAdapter... os argumentos esperados são os rótulos dos atributos. Tome muito cuidado neste ponto, pois se fosse colocado somente, por exemplo, "text", todos os TextViews que tivessem o atributo android:text sendo utilizado acionariam o método loadRemoteImage().

Logo, o que recomendo é que em caso de necessidade de um método @BindingAdapter, busque utilizar um nome específico para o atributo.

Outra coisa: o namespace não é necessário em @BindingAdapter, mas eu poderia colocar "app:picassoLoad", por exemplo, se o namespace em uso fosse app:.

Os parâmetros do método BindAdapter são dispostos na seguinte ordem:

  • Primeiro o objeto da View que tem o listener de atributo;
  • Os parâmetros seguintes são os valores colocados nos atributos referenciados em @BindingAdapter, na ordem de referência.

Caso fosse necessário definir mais atributos, somente teríamos de defini-los com a separação por vírgula, veja a seguir:

...
@BindingAdapter( "picassoLoad", "app:loadPicasso" )
fun loadRemoteImage( iv: ImageView, url: String, url2: String ){
...
}
...

 

Para que um método BindAdapter seja acionado todos os atributos declarados nele devem estar presentes na View ou então devemos explicitar em código que não há necessidade de que todos os atributos estejam sendo utilizados, como a seguir:

...
@BindingAdapter( value = ["picassoLoad", "app:loadPicasso"], requireAll = false )
fun loadRemoteImage( iv: ImageView, url: String?, url2: String? ){
...
}
...

 

Note que requireAll, por padrão, é true. Veja também a necessidade de usarmos o operador null-safe, ?, pois quando um atributo não for definido o método será invocado mesmo assim, mas com o valor padrão para o tipo do atributo não definido.

Ainda temos de adicionar a propriedade de imagem a classe User:

...
open class User(
val firstName: String,
val lastName: String,
val imageUrl: String = "https://www.thiengo.com.br/img/system/logo/logo-thiengo-calopsita-70x70.png",
val age: Int = 20,
val isAdult: Boolean = true ) {
...
}
...

 

E também adicionar o ImageView ao layout:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
...
<LinearLayout ...>
...
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
app:picassoLoad="@{ user.imageUrl }" />
</LinearLayout>
</layout>

 

Lembrando que o atributo picassoLoad não foi criado em lugar algum, simplesmente escolhi este rótulo, pois condiz com a funcionalidade do método BindAdapter. Não é preciso criar o novo atributo em algum local específico do projeto.

Executando o projeto com o novo código, temos:

Carregamento de imagem via BindingAdapter

Note que apesar de o método BindAdapter criado estar dentro da classe BindListener, métodos BindAdapter não necessitam da vinculação em código, digo, vinculação como fizemos no onCreate() anteriormente:

...
binding.user = user
binding.bindListener = BindListener()
...

 

E é devido a essa particularidade de @BindingAdapter que nós desenvolvedores temos de ter cautela no uso dele, pois é possível termos os mesmos problemas encontrados no uso de go to ou de "variável global".

Trabalhando com o vinculo de entidades estáticas

Para invocar métodos estáticos, sem o uso de @BindAdapter, ou até mesmo propriedades, basta adicionar a classe container de entidades estáticas via <import> e então trabalhar as referências. Veja a nova classe a seguir:

class Util {
companion object {
@JvmStatic
val CONSTANT = "Constante"

@JvmStatic
fun listenerClick( view: View, user: User ){
Toast
.makeText(
view.context,
"${user.firstName} ${user.lastName}",
Toast.LENGTH_SHORT
)
.show()
}
}
}

 

Então a atualização do layout que estamos trabalhando como layout de exemplo:

<?xml version="1.0" encoding="utf-8"?>
<layout ...>
<data>
...
<import type="thiengo.com.br.databindingtests.domain.Util" />
...
</data>

<LinearLayout ...>
...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:onClick="@{ (view)->Util.listenerClick( view, user ) }"
android:text="@{ Util.CONSTANT }" />
</LinearLayout>
</layout>

 

Executando o projeto e clicando em "Constante", temos:

Entidades estáticas via Data Binding

Bind via <include>

Para aproveitar variáveis já declaradas em um layout pai, podemos passa-las por meio de <include> com um atributo referente ao nome da variável.

Veja a seguir o novo layout de nome completo, /res/layout/full_name.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="thiengo.com.br.databindingtests.domain.User" />

<variable
name="userInside"
type="User" />
</data>

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

<TextView
android:id="@+id/tv_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffff00"
android:padding="16dp"
android:text="@{ userInside.fullName() }" />
</LinearLayout>
</layout>

 

Veja como ainda é necessária toda a configuração de tags (<layout>, <data>, <import> e <variable>) para uso de uma variável herdada do layout pai.

Então o código de inclusão do layout anterior no XML principal, incluindo o envio da variável user:

<?xml version="1.0" encoding="utf-8"?>
<layout ...>
...
<LinearLayout ...>
...
<!--
O rótulo do atributo que receberá a variável tem que ser
o mesmo rótulo definido para a variável no layout incluído
via <include>.
-->
<include
app:userInside="@{ user }"
layout="@layout/full_name" />
</LinearLayout>
</layout>

 

Executando o projeto com o novo código, temos:

Binding via tag include

Expressões em sintaxe Java

Apesar de a linguagem dinâmica em uso nos exemplos aqui ser Kotlin, as expressões dentro de @{} são em maioria em Java, digo em maioria, pois há a possibilidade de uso do operador null-safe, ?, neste caso utilizando duas interrogações, ??.

Veja a seguir um código de exemplo fazendo uso do operador ternário, não disponível em Kotlin, mas em Java. Código adicionado ao layout principal abordado até aqui, activity_main.xml:

...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text='@{ user.isAdult ? "É Adulto" : "Não é adulto" }' />
...

 

Veja que quando for necessário utilizar uma String dentro da expressão em @{} o roteiro é utilizar aspas simples, ', como aspas principais e as aspas duplas, ", para a String.

Executando o projeto com o código anterior, temos:

Expressões Java em Data Binding

Veja uma outra maneira de fornecer o nome completo do usuário:

...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text='@{ String.format( "Nome completo: %s %s", user.firstName, user.lastName ) }' />
...

 

Lembrando que String faz parte do pacote java.lang e assim não precisa de um <import> no bloco <data>.

Executando o projeto com o novo código, temos:

Expressões Java em Data Binding

Também é possível aplicar casting no modelo Java:

...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text='@{ ((User) person).firstName }' />
...

 

Mesmo no cast, se o tipo referenciado não estiver presente no pacote java.lang e nos imports já incluídos no bloco <data>, um <import> para esse tipo será necessário.

Importante:

Tome cuidado com as possibilidades de código como expressão em @{}, pois o objetivo do Data Binding é facilitar as coisas, ou seja, se houverem expressões complexas sendo utilizadas no binding é bem provável que o uso da biblioteca traga mais pontos negativos, em termos de leitura de código, do que pontos positivos.

No projeto de exemplo na segunda parte do artigo, terá um ponto onde será preferível continuar com o código já definido, sem uso da Data Binding API, pois ele se apresentará como uma opção mais simples.

Se for necessária a comparação utilizando os sinais de maior, >, e menor, <, na verdade os códigos HTML destes símbolos, respectivamente &gt; e &lt;, é que deverão ser utilizados. Veja o exemplo a seguir:

...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text='@{ user.age >= 18 ? "É adulto" : "Não é adulto" }' />
...

 

O código acima não compila, pois os símbolos > e < não são processáveis como parte de uma expressão. O código a seguir compila sem problemas:

...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text='@{ user.age &gt;= 18 ? "É adulto" : "Não é adulto" }' />
...

Objeto de contexto

Quando desenvolvendo aplicativos Android é comum que seja necessário o uso do objeto de contexto para acesso a algum recurso.

A API Data Binding disponibiliza para nós o objeto context representando o contexto da atividade.

Por questões de didática vamos modificar o método mudancaStatus() de BindListener para fazer uso de um contexto passado como parâmetro:

...
fun mudancaStatus( context: Context, checkBox: CompoundButton, status: Boolean ) {

Toast
.makeText( context, "Novo status: $status", Toast.LENGTH_SHORT )
.show()
}
...

 

Agora a atualização no layout:

...
<CheckBox
android:text="Apenas um teste com o CheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked)->bindListener.mudancaStatus( context, cb, isChecked )}" />
...

 

Executando o projeto e acionando o CheckBox, temos:

Objeto de contexto via Data Binding

Binding no menu gaveta, NavigationView

A partir desta seção abordarei alguns exemplos de Data Binding que certamente serão úteis a ti, pois é a aplicação do binding em componentes visuais comumente utilizados no desenvolvimento Android.

Veja a seguir o código XML de cabeçalho de um menu gaveta, código já com a configuração Data Binding. O path do layout de cabeçalho é /res/layout/nav_header.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<variable
name="user"
type="thiengo.com.br.testnavigationview.User" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@{ user.getImageSource( context ) }" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{ user.name }" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ user.email }" />
</LinearLayout>
</layout>

 

Por motivos de brevidade no exemplo vamos omitir a apresentação da nova classe User. Nossa prioridade aqui é mostrar como acessar a classe binding de cabeçalho de menu gaveta para então vincular um objeto a ela.

A seguir o layout que contém o NavigationView que referencia o layout de cabeçalho apresentado acima:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
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"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">

<include
layout="@layout/app_bar_acitivity"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header"
app:menu="@menu/activity_tenis_list_drawer" />
</android.support.v4.widget.DrawerLayout>

 

Agora o modo de acesso, em código dinâmico, a classe binding referente ao layout de cabeçalho:

...
override fun onCreate( savedInstanceState: Bundle? ) {
...

/*
* Realizando o bind somente do layout de cabeçalho
* do menu gaveta.
* */
val navHeader = NavHeaderBinding.bind( nav_view.getHeaderView(0) )

navHeader.user = user
}
...

 

A invocação getHeaderView(0) retorna o objeto ViewGroup root do layout de cabeçalho, aqui o LinearLayout.

Como não havia necessidade de bind de dados e métodos no layout ancestral do layout de cabeçalho, não foi necessária a inclusão de código Data Binding nele.

Binding em caixa de diálogo

A seguir um simples layout personalizado para um AlertDialog, layout já com os códigos Data Binding. Segue /res/layout/dialog_custom.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<variable
name="user"
type="thiengo.com.br.testdialog.User" />
</data>

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

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{ user.firstName }" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{ user.lastName }" />
</LinearLayout>
</layout>

 

Então o código dinâmico que contém a classe binding do layout anterior:

...
override fun onCreate( savedInstanceState: Bundle? ) {
...

/*
* Inflando o layout junto a classe binding gerada.
* */
val binding = DialogCustomBinding.inflate( layoutInflater )

binding.user = user

val builder = AlertDialog.Builder( this )

/*
* Colocando o layout bound como custom layout do
* AlertDialog.
* */
builder.setView( binding.root )

builder.create().show()
}
...

Binding em adapter de lista, RecyclerView

A seguir o código XML do layout de item do adapter de framework de lista, código já com sintaxe Data Binding. Segue /res/layout/user.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<variable
name="user"
type="thiengo.com.br.testadapter.User" />
</data>

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

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{ user.firstName }" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{ user.lastName }" />
</LinearLayout>
</layout>

 

Agora o código da classe adapter com o vinculo Data Binding:

class UsersAdapter(
private val context: Context,
private val users: List<User> ) :
RecyclerView.Adapter<UsersAdapter.ViewHolder>() {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int ) : UsersAdapter.ViewHolder {

val layoutInflater = LayoutInflater.from( context )

/*
* Vinculando o layout de item a classe binding dela.
* */
val userBinding = UserBinding
.inflate( layoutInflater, parent, false )

return ViewHolder( userBinding )
}

override fun onBindViewHolder(
holder: ViewHolder,
position: Int ) {

holder.setData( users[ position ] )
}

override fun getItemCount(): Int {
return users.size
}

inner class ViewHolder( val binding: UserBinding ) :
RecyclerView.ViewHolder( binding.root ) {

fun setData( user: User ) {
binding.user = user

/*
* O método executePendingBindings() força o
* binding a ser executado imediatamente, em
* vez de atrasá-lo até o próximo quadro.
* */
binding.executePendingBindings()
}
}
}

 

Note a importância da invocação do método executePendingBindings() para que a atualização de item ocorra imediatamente. Este método deve ser invocado de maneira direta ou indireta pelo onBindViewHolder().

A seguir um exemplo de binding de dados via Data Binding API quando temos mais de um layout de item, mais precisamente, em nosso exemplo, os layouts header.xml e regular_item.xml. Segue classe adaptadora:

class CustomAdapter(
private val context: Context,
private val items: List<Item>
) : RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() {

companion object {
const val TYPE_HEADER = 0
const val TYPE_REGULAR_ITEM = 1
}

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int ): CustomViewHolder {

val inflater = LayoutInflater.from( context )

/*
* Todas as classes binding geradas são subclasses
* de ViewDataBinding.
* */
val binding: ViewDataBinding

when( viewType ) {
TYPE_HEADER -> {
binding = DataBindingUtil
.inflate(
inflater,
R.layout.header,
parent, false
)

return CustomViewHolder( binding as HeaderBinding )
}
else -> {
binding = DataBindingUtil
.inflate(
inflater,
R.layout.regular_item,
parent,
false
)

return CustomViewHolder( binding as RegularItemBinding )
}
}
}

override fun onBindViewHolder(
holder: CustomViewHolder,
position: Int ) {

val item = items[ position ]

if( getItemViewType( position ) == TYPE_HEADER ){
holder.setHeader( item )
}
else{
holder.setItem( item )
}
}

override fun getItemViewType( position: Int ): Int =
if (position == 0) {
TYPE_HEADER
}
else {
TYPE_REGULAR_ITEM
}

override fun getItemCount(): Int {
return items.size
}

inner class CustomViewHolder : RecyclerView.ViewHolder {

lateinit var headerBinding: HeaderBinding
lateinit var regularItemBinding: RegularItemBinding

constructor( binding: HeaderBinding ) : super( binding.getRoot() ) {
headerBinding = binding
}

constructor( binding: RegularItemBinding ) : super( binding.getRoot() ) {
regularItemBinding = binding
}

fun setHeader( item: Item ){
headerBinding.item = item
headerBinding.executePendingBindings()
}

fun setItem( item: Item ){
regularItemBinding.item = item
regularItemBinding.executePendingBindings()
}
}
}

 

Como informado no código acima, todas as classes binding são subclasses de ViewDataBinding.

Binding em fragmento

A seguir um layout de exemplo já com o código binding. Segue /res/layout/fragment_user.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<variable
name="user"
type="thiengo.com.br.testfragment.User" />
</data>

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

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{ user.firstName }" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{ user.lastName }" />
</LinearLayout>
</layout>

 

Então o código dinâmico de binding de classe:

...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedInstanceState: Bundle ): View {

val binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_user,
container,
false
)

binding.user = user

/*
* Caso seja necessário alguma configuração ainda em
* código dinâmico, root é o ViewGroup container do
* layout do fragmento.
* */
val view = binding.root

/* TODO */

return view /* Ou binding.root */
}
...

Pontos negativos

  • Não é possível ter acesso, no código XML, ao objeto da atividade host, mesmo quando aplicando casting na propriedade context;
  • A documentação falhou muito na apresentação da sintaxe de entidades estáticas para Data Binding em Kotlin. Não há nada sobre a necessidade do companion object e nem sobre a anotação @JvmStatic;
  • Depois de muitos testes é percebido que métodos ouvidores muitas vezes são mais eficientes em leitura de código se utilizando o objeto View e o método de listener tradicionais, sem o auxílio do Data Binding.

Pontos positivos

  • O código dinâmico (Java, Kotlin) realmente fica mais simples sem a necessidade de códigos tradicionais de binding de dados;
  • A sintaxe de uso da API é bem simples;
  • O suporte a layouts nos mais diferentes lugares (fragment, atividade, dialog, adapter, ...) ajuda em muito no uso completo da biblioteca;
  • Métodos @BindingAdapter e a propriedade context facilitam em muito o vinculo de métodos com códigos e assinaturas mais complexas.

Considerações finais

Neste artigo optei por abordar as partes mais importantes para o uso imediato da biblioteca Data Binding, isso, pois a biblioteca e documentação dela são bem maiores.

Logo, sem o objetivo de ter aqui uma fonte única de estudos sobre a Data Binding API, mesmo acreditando ter abordado o essencial, não deixe de também acessar a documentação para estudar ao menos algumas características mais complexas.

De qualquer forma, conhecer e dominar a biblioteca Data Binding no Android pode até mesmo lhe proporcionar melhores posições no mercado de desenvolvedor mobile.

A API é excelente, permite o uso mesclado de binding via API e binding via modelo tradicional (utilizando findViewById(), por exemplo) e não tem comprometimentos em termos de performance de processamento.

Projeto Android

No projeto de exemplo teremos um aplicativo similar a um app real Android de venda de tênis. Mesmo os dados do app sendo locais será possível trabalhar a biblioteca Data Binding nele, pois há inúmeros códigos de vinculo de dados e métodos.

O projeto será desenvolvido em duas etapas:

  • Na primeira teremos todo o código sem uso da Data Binding API;
  • Na segunda vamos a implantação de Data Binding, sempre que possível.

O projeto está presente no GitHub a seguir: https://github.com/viniciusthiengo/tenis-mob-shop.

Mesmo com o projeto já pronto no GitHub, não deixe de seguir o exemplo em artigo, pois nele iremos passo a passo explicando os trechos de código em uso.

Protótipo estático

A seguir as imagens do protótipo estático da primeira parte do projeto de app:

Tela de entrada

Tela de entrada

Tela de listagem de tênis

Tela de listagem de tênis

Menu gaveta aberto

Menu gaveta aberto

Tela de detalhes de tênis

Tela de detalhes de tênis

Compartilhamento acionado

 Compartilhamento acionado

Caixa de diálogo para pagamento

Caixa de diálogo para pagamento

Tela de Obrigado pela compra

Tela de "Obrigado pela compra"

 


Iniciando o projeto

Em seu Android Studio inicie um novo projeto Kotlin:

  • Nome da aplicação: Tênis Mob Shop;
  • API mínima: 16 (Android Jelly Bean). Mais de 99% dos aparelhos Android em mercado sendo atendidos;
  • Atividade inicial: Navigation Drawer Activity;
  • Nome da atividade inicial: SneakersActivity. O nome do layout da atividade inicial será atualizado automaticamente;
  • Para todos os outros campos, deixe-os com os valores já definidos por padrão.

Ao final do projeto teremos a seguinte arquitetura:

Arquitetura Android Studio do projeto

Configurações Gradle

A seguir as configurações iniciais do Gradle Project Level, ou build.gradle (Project: TnisMobShop):

buildscript {
ext.kotlin_version = '1.2.60'
ext.android_plugin_version = '3.1.4'

repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:$android_plugin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()

/* Para a RoundedImageView */
mavenCentral()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

Então as configurações iniciais do Gradle App Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 28
defaultConfig {
applicationId "thiengo.com.br.tnismobshop"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
implementation 'com.android.support:design:28.0.0-rc01'

/* Para a RoundedImageView */
implementation 'com.makeramen:roundedimageview:2.3.0'
}

 

Note que estamos utilizando a RoundedImageView API para obter as bordas arredondadas definidas em protótipo estático. Não esqueça de sempre utilizar as versões mais atuais e estáveis de APIs e SDKs referenciados.

Configurações AndroidManifest

A seguir as configurações do AndroidManifest.xml que para este projeto é um pouco maior do que o comumente mostrado aqui no Blog, isso devido a quantidade de atividades do app de exemplo:

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.tnismobshop">

<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".SneakersActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".SneakerDetailsActivity"
android:parentActivityName=".SneakersActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="thiengo.com.br.tnismobshop.SneakersActivity" />
</activity>

<activity
android:name=".ThankYouActivity"
android:label="@string/title_activity_thank_you"
android:parentActivityName=".SneakersActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="thiengo.com.br.tnismobshop.SneakersActivity" />
</activity>
</application>
</manifest>

Configurações de estilo

As configurações de tema são simples, mas grandes. Isso pois há bastante texto e estilo, <style>, no projeto.

Iniciando com o arquivo de definição de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#388E3C</color>
<color name="colorPrimaryDark">#00600F</color>
<color name="colorPrimaryLight">#81E281</color>
<color name="colorAccent">#1976D2</color>

<color name="colorAccentDark">#00600F</color>

<color name="colorDarkDialog">#5B646E</color>

<color name="colorBgNavigationView">#F5F5F7</color>

<color name="colorContentLabel">#CCCCCC</color>

<color name="colorFreeDelivery">#CCFFFFFF</color>

<color name="colorDialogLine">#E4E6E7</color>
<color name="colorDialogPopUpSpinner">#EEEEEE</color>
</resources>

 

Então o arquivo de dimensões, /res/values/dimens.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>

<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen>

<dimen name="fab_margin">16dp</dimen>
</resources>

 

Agora o arquivo de arrays, /res/values/arrays.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="colors">
<item>Preto</item>
<item>Vermelho</item>
<item>Branco</item>
<item>Azul</item>
<item>Verde</item>
</string-array>

<string-array name="sizes">
<item>36</item>
<item>37</item>
<item>38</item>
<item>39</item>
<item>40</item>
<item>41</item>
<item>42</item>
<item>43</item>
<item>44</item>
</string-array>

<string-array name="amount">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</string-array>

<string-array name="months">
<item>01</item>
<item>02</item>
<item>03</item>
<item>04</item>
<item>05</item>
<item>06</item>
<item>07</item>
<item>08</item>
<item>09</item>
<item>10</item>
<item>11</item>
<item>12</item>
</string-array>

<string-array name="years">
<item>2018</item>
<item>2019</item>
<item>2020</item>
<item>2021</item>
<item>2022</item>
<item>2023</item>
<item>2024</item>
<item>2025</item>
<item>2026</item>
</string-array>

<string-array name="parcels">
<item>1x</item>
<item>2x</item>
<item>3x</item>
<item>4x</item>
<item>5x</item>
<item>6x</item>
<item>7x</item>
<item>8x</item>
<item>9x</item>
<item>10x</item>
<item>11x (2% juros)</item>
<item>12x (2.5% juros)</item>
</string-array>
</resources>

 

Assim o extenso arquivo de Strings, /res/values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Tênis Mob Shop</string>

<string name="title_activity_thank_you">Voltar para Tênis Shop</string>

<string name="navigation_drawer_open">Menu gaveta aberto</string>
<string name="navigation_drawer_close">Menu gaveta fechado</string>

<string name="action_settings">Settings</string>

<string name="desc_logo_app">Logo Tênis Shop App</string>

<string name="desc_sneaker_first_gallery_image">
Primeira imagem extra da galeria
</string>
<string name="desc_sneaker_second_gallery_image">
Segunda imagem extra da galeria
</string>
<string name="desc_sneaker_third_gallery_image">
Terceira imagem extra da galeria
</string>

<string name="desc_male_icon">Ícone do sexo masculino</string>
<string name="desc_female_icon">Ícone do sexo feminino</string>

<string name="label_free">grátis</string>
<string name="label_put_on_shop_cart">Colocar no</string>

<string name="brazil_free_delivery">Frete grátis para todo Brasil</string>
<string name="desc_truck_delivery">Ícone de caminhão para entrega, frete</string>

<string name="label_recommended">Recomendado para:</string>
<string name="label_type">Tipo:</string>
<string name="label_composition">Composição:</string>
<string name="label_access_rating">Acessar avaliações</string>
<string name="label_buy">Comprar</string>
<string name="label_description">Descrição:</string>

<string name="value_stok_invetory_amount">Quantidade</string>
<string name="value_stok_invetory_end">em estoque</string>
<string name="value_stok_invetory">Quantidade (4 estoque)</string>
<string name="value_recommended">Running</string>
<string name="value_type">Performance</string>
<string name="value_composition">68% Poliamida | 32% Sintético</string>
<string name="value_desc">
O mais novo Fresh Foam Cruz conta com a nossa
premiada tecnologia Fresh Foam para uma corrida
estável e de alto amortecimento. Seu cabedal
sofisticado combina a junção central do tênis
com o mesh respirável para oferecer suporte com
uma camada extra de conforto. O Fresh Foam Cruz
proporciona uma sensação de frescor ao seu pé,
treino após treino.
</string>

<string name="excellent_buy">Excelente compra!</string>
<string name="keep_buying">Continuar comprando</string>
<string name="dialog_title">Pagamento</string>
<string name="desc_credit_cards">Bandeiras de cartão aceitas</string>

<string name="dialog_label_card_number">Número do cartão:</string>
<string name="dialog_hint_card_number">xxxx xxxx xxxx xxxx</string>
<string name="dialog_label_card_cvv">Código de segurança - CVV:</string>
<string name="dialog_hint_card_cvv">xxx ou xxxx</string>
<string name="dialog_label_card_owner">
Nome do proprietário do cartão - como está no cartão:
</string>
<string name="dialog_label_card_expire">Vencimento cartão</string>
<string name="dialog_label_card_expire_month">Mês:</string>
<string name="dialog_label_card_expire_bar">/</string>
<string name="dialog_label_card_expire_year">Ano:</string>
<string name="dialog_label_card_total_parcels">
<![CDATA[Número de parcelas. Se > 10x tem adição de juros:]]>
</string>

<string name="label_bt_finish_buy">Finalizar compra</string>

<string name="dialog_label_total">Total:</string>
<string name="dialog_label_parcels_without_taxes">
em até 10x sem juros
</string>

<string name="desc_sneaker_brand">Marca do tênis</string>
<string name="desc_sneaker_bought">Tênis comprado</string>
<string name="desc_sneaker_bought_brand">Marca do tênis comprado</string>
<string name="label_support_shop">Tel. suporte: 0800 723 6659</string>

<string name="item_add_to_cart">Adicionar ao carrinho</string>
<string name="item_search">Buscar</string>
<string name="item_cart">Carrinho</string>

<string name="track_code_label">Código de rastreamento:</string>
<string name="sneacker_label">Tênis:</string>

<string name="share_content_shared">Conteúdo compartilhado</string>
<string name="share_label">Compartilhar</string>

<string name="until">até</string>
<string name="or_in">ou em</string>

<string name="sneaker">Tênis</string>

<string name="nav_item_all_brands">Todas as marcas</string>
<string name="nav_item_male">Masculino</string>
<string name="nav_item_female">Feminino</string>
<string name="nav_item_child">Infantil</string>
<string name="nav_group_config">Configurações</string>
<string name="nav_item_app">Aplicativo</string>
<string name="nav_item_account">Conta</string>
</resources>

 

Agora o arquivo de definição de algumas configurações de estilo para versões do Android Lollipop ou superior, /res/values-v21/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
</resources>

 

Por fim o arquivo de definição geral de tema de projeto, o extenso arquivo /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

<!--
Estilo padrão, aplicado em todo o projeto.
-->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

<item name="android:windowBackground">@drawable/background</item>

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

<!--
Para que a barra de topo padrão não seja utilizada e
assim somente o AppBarLayout junto ao Toolbar possam ser
usados.
-->
<style name="AppTheme.NoActionBar">

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<!--
Para o correto enquadramento do AppBarLayout.
-->
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<!--
Utilizado para a correta apresentação de menus de pop-up
em barra de topo.
-->
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />

<!--
Configurações para Spinners com fundo branco.
-->
<style name="AppTheme.SpinnerTheme" parent="AppTheme">
<item name="android:textViewStyle">@style/AppTheme.TextViewStyle</item>
</style>

<style name="AppTheme.TextViewStyle" parent="android:Widget.TextView">
<item name="android:textColor">@android:color/white</item>
</style>

<!--
Configurações para Spinners com fundo vermelho.
-->
<style name="AppTheme.SpinnerThemeDark">
<item name="android:textViewStyle">@style/AppTheme.TextViewStyleDark</item>
</style>

<style name="AppTheme.TextViewStyleDark" parent="android:Widget.TextView">
<item name="android:textColor">@color/colorDarkDialog</item>
</style>

<!--
Estilo com as configurações das pequenas imagens de galeria,
imagens presentes nos itens de lista.
-->
<style name="AppTheme.ItemImgGallery">
<item name="android:layout_width">23dp</item>
<item name="android:layout_height">23dp</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:background">@drawable/image_view_sneaker</item>
<item name="android:layout_toRightOf">@+id/iv_sneaker</item>
</style>

<!--
Estilo com as configurações das pequenas imagens de galeria,
imagens presentes na atividade de detalhes.
-->
<style name="AppTheme.DetailsImgGallery" parent="AppTheme.ItemImgGallery">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">0dp</item>
<item name="android:layout_weight">1</item>
</style>

<!--
Estilo com as configurações de ícone de gênero em itens
de lista.
-->
<style name="AppTheme.ItemIconGenre">
<item name="android:layout_width">16dp</item>
<item name="android:layout_height">16dp</item>
<item name="android:layout_below">@+id/iv_brand</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:visibility">gone</item>
</style>

<!--
Estilo com as configurações de ícone de gênero presentes
na atividade de detalhes.
-->
<style name="AppTheme.DetailsIconGenre" parent="AppTheme.ItemIconGenre">
<item name="android:layout_width">40dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:layout_alignParentLeft">true</item>
<item name="android:padding">9dp</item>
</style>

<!--
Estilo com as configurações de ícones de estrela para
apresentar as avaliações dos usuários. Ícones de estrela
em item de lista.
-->
<style name="AppTheme.ItemStar">
<item name="android:layout_width">18dp</item>
<item name="android:layout_height">18dp</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:layout_marginLeft">1dp</item>
</style>

<!--
Estilo com as configurações de ícones de estrela para
apresentar as avaliações dos usuários. Ícones de estrela
presentes na atividade de detalhes.
-->
<style name="AppTheme.DetailsStar" parent="AppTheme.ItemStar">
<item name="android:layout_alignParentTop">true</item>
</style>

<!--
Estilo com as configurações das grandes linhas verticais
da atividade de detalhes.
-->
<style name="AppTheme.DetailsVerticalLine">
<item name="android:layout_width">1dp</item>
<item name="android:layout_height">240dp</item>
<item name="android:layout_alignParentTop">true</item>
<item name="android:layout_marginLeft">14dp</item>
<item name="android:layout_marginRight">14dp</item>
<item name="android:background">@android:color/white</item>
</style>

<!--
Estilo com as configurações das pequenas linhas verticais
da atividade de detalhes.
-->
<style name="AppTheme.DetailsSmalVerticalLine">
<item name="android:layout_width">1dp</item>
<item name="android:layout_height">36dp</item>
<item name="android:layout_marginTop">6dp</item>
<item name="android:background">@android:color/white</item>
<item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_marginRight">8dp</item>
</style>

<!--
Estilo com as configurações das grandes linhas horizontais
da atividade de detalhes.
-->
<style name="AppTheme.DetailsHorizontalLine">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1dp</item>
<item name="android:layout_alignParentLeft">true</item>
<item name="android:background">@android:color/white</item>
<item name="android:layout_marginBottom">9dp</item>
</style>

<!--
Estilo com as configurações dos rótulos de conteúdos da
atividade de detalhes.
-->
<style name="AppTheme.DetailsContentLabel">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_alignParentLeft">true</item>
<item name="android:layout_marginRight">4dp</item>
<item name="android:textColor">@color/colorContentLabel</item>
</style>

<!--
Estilo com as configurações dos TextViews de conteúdos da
atividade de detalhes.
-->
<style name="AppTheme.DetailsContent">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">@android:color/white</item>
</style>

<!--
Estilo com as configurações dos botões verdes da atividade
de detalhes.
-->
<style name="AppTheme.DetailsButton">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@drawable/button_blue_background</item>
<item name="android:textAllCaps">false</item>
<item name="android:textColor">@android:color/white</item>
</style>
</resources>

 

Com os vários estilos definidos acima os códigos XML ficam mais simples de se manter e também facilitam o trabalho com a sintaxe Data Binding, que exige ainda mais atributos.

Classes de domínio

Teremos cinco classes de domínio, quatro delas implementando a Interface Parcelable. Todas estarão no pacote /domain.

Vamos começar pela mais simples, responsável por manter os dados de usuário, User:

data class User(
val name: String,
val email: String,
val image: Int )

 

Agora a classe de avaliação de tênis, Rating:

class Rating(
val amount: Int,
val stars: Int ) : Parcelable {

constructor( source: Parcel ) : this(
source.readInt(),
source.readInt()
)

override fun describeContents() = 0

override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeInt( amount )
writeInt( stars )
}

companion object {
@JvmField
val CREATOR: Parcelable.Creator<Rating> = object : Parcelable.Creator<Rating> {
override fun createFromParcel( source: Parcel ): Rating = Rating( source )
override fun newArray( size: Int ): Array<Rating?> = arrayOfNulls( size )
}
}
}

 

Assim a classe de marca de tênis, Brand:

class Brand(
val imageResource: Int,
val name: String ) : Parcelable {

constructor( source: Parcel ) : this(
source.readInt(),
source.readString()
)

override fun describeContents() = 0

override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeInt( imageResource )
writeString( name )
}

companion object {
@JvmField
val CREATOR: Parcelable.Creator<Brand> = object : Parcelable.Creator<Brand> {
override fun createFromParcel( source: Parcel ): Brand = Brand( source )
override fun newArray( size: Int ): Array<Brand?> = arrayOfNulls( size )
}
}
}

 

Agora a classe de informações extras de tênis, ExtraInfo:

class ExtraInfo(
val stock: Int,
val recommended: String,
val type: String,
val composition: String,
val fullDescription: String ) : Parcelable {

/*
* Obtém o dado de estoque formatado para a apresentação
* a humanos.
* */
fun getStockFormatted( context: Context ) : String =
String.format(
"%s (%d %s)",
context.getString( R.string.value_stok_invetory_amount ),
stock,
context.getString( R.string.value_stok_invetory_end )
)

constructor( source: Parcel ) : this(
source.readInt(),
source.readString(),
source.readString(),
source.readString(),
source.readString()
)

override fun describeContents() = 0

override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeInt( stock )
writeString( recommended )
writeString( type )
writeString( composition )
writeString( fullDescription )
}

companion object {
@JvmField
val CREATOR: Parcelable.Creator<ExtraInfo> = object : Parcelable.Creator<ExtraInfo> {
override fun createFromParcel( source: Parcel ): ExtraInfo = ExtraInfo( source )
override fun newArray( size: Int ): Array<ExtraInfo?> = arrayOfNulls( size )
}
}
}

 

E por fim a classe host dos dados de tênis, Sneaker:

class Sneaker(
val imageResource: Int,
val album: IntArray,
val model: String,
val brand: Brand,
val isForMale: Boolean,
val isForFemale: Boolean,
val rating: Rating,
val price: Double,
val extraInfo: ExtraInfo ) : Parcelable {

/*
* Método responsável por retornar o valor do tênis
* em um formato humano, brasileiro.
* */
fun getPriceAsString() : String =
String.format( Locale.FRANCE, "R$ %.2f", price )

/*
* Método responsável por retornar o valor do tênis,
* em parcelas de 10x, em um formato humano, brasileiro.
* */
fun getPriceParcelsAsString() : String {
val value = price / 10
return String.format( Locale.FRANCE, "10x R$ %.2f", value )
}

constructor( source: Parcel ) : this(
source.readInt(),
source.createIntArray(),
source.readString(),
source.readParcelable<Brand>( Brand::class.java.classLoader ),
1 == source.readInt(),
1 == source.readInt(),
source.readParcelable<Rating>( Rating::class.java.classLoader ),
source.readDouble(),
source.readParcelable<ExtraInfo>( ExtraInfo::class.java.classLoader )
)

override fun describeContents() = 0

override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeInt( imageResource )
writeIntArray( album )
writeString( model )
writeParcelable( brand, 0 )
writeInt( (if(isForMale) 1 else 0) )
writeInt( (if(isForFemale) 1 else 0) )
writeParcelable( rating, 0 )
writeDouble( price )
writeParcelable( extraInfo, 0 )
}

companion object {
val KEY = "SNEAKER_KEY"
val KEY_AMOUNT = "AMOUNT_SNEAKERS_KEY"

@JvmField
val CREATOR: Parcelable.Creator<Sneaker> = object : Parcelable.Creator<Sneaker> {
override fun createFromParcel( source: Parcel ): Sneaker = Sneaker( source )
override fun newArray( size: Int ): Array<Sneaker?> = arrayOfNulls( size )
}
}
}

Base de dados simulados, mock data

Para nosso projeto de exemplo vamos utilizar uma base de dados mock, simulados. No pacote /data crie a classe Database como a seguir:

class Database {
companion object {

/*
* Retorna os dados de teste do aplicativo, dados
* simulados ou dados mock
* */
fun getSneakers( context: Context ) =
listOf(
Sneaker(
R.drawable.shoes_01_a,
intArrayOf(R.drawable.shoes_01_b, R.drawable.shoes_01_c, R.drawable.shoes_01_d),
"Fresh Foam Cruz",
Brand(R.drawable.ic_new_balance, "New Balance"),
true,
true,
Rating(42, 5),
499.90,
ExtraInfo(
4,
context.getString(R.string.value_recommended),
context.getString(R.string.value_type),
context.getString(R.string.value_composition),
context.getString(R.string.value_desc)
)
),
Sneaker(
R.drawable.shoes_02_a,
intArrayOf(R.drawable.shoes_02_b, R.drawable.shoes_02_c, R.drawable.shoes_02_d),
"Epic React Flyknit",
Brand(R.drawable.ic_nike, "Nike"),
true,
false,
Rating(91, 5),
699.90,
ExtraInfo(
4,
context.getString(R.string.value_recommended),
context.getString(R.string.value_type),
context.getString(R.string.value_composition),
context.getString(R.string.value_desc)
)
),
Sneaker(
R.drawable.shoes_03_a,
intArrayOf(R.drawable.shoes_03_b, R.drawable.shoes_03_c, R.drawable.shoes_03_d),
"Supernova",
Brand(R.drawable.ic_adidas, "Adidas"),
true,
false,
Rating(29, 3),
599.99,
ExtraInfo(
4,
context.getString(R.string.value_recommended),
context.getString(R.string.value_type),
context.getString(R.string.value_composition),
context.getString(R.string.value_desc)
)
),
Sneaker(
R.drawable.shoes_04_a,
intArrayOf(R.drawable.shoes_04_b, R.drawable.shoes_04_c, R.drawable.shoes_04_d),
"GEL-Kenun",
Brand(R.drawable.ic_asics, "Asics"),
true,
false,
Rating(84, 4),
699.90,
ExtraInfo(
4,
context.getString(R.string.value_recommended),
context.getString(R.string.value_type),
context.getString(R.string.value_composition),
context.getString(R.string.value_desc)
)
),
Sneaker(
R.drawable.shoes_05_a,
intArrayOf(R.drawable.shoes_05_b, R.drawable.shoes_05_c, R.drawable.shoes_05_d),
"Charged Bandit 3",
Brand(R.drawable.ic_under_armour, "UnderArmour"),
false,
true,
Rating(19, 5),
349.90,
ExtraInfo(
4,
context.getString(R.string.value_recommended),
context.getString(R.string.value_type),
context.getString(R.string.value_composition),
context.getString(R.string.value_desc)
)
),
Sneaker(
R.drawable.shoes_06_a,
intArrayOf(R.drawable.shoes_06_b, R.drawable.shoes_06_c, R.drawable.shoes_06_d),
"Wave Sky",
Brand(R.drawable.ic_mizuno, "Mizuno"),
true,
true,
Rating(42, 5),
749.99,
ExtraInfo(
4,
context.getString(R.string.value_recommended),
context.getString(R.string.value_type),
context.getString(R.string.value_composition),
context.getString(R.string.value_desc)
)
)
)

/*
* Retorna o objeto usuário responsável por simular
* o usuário conectado.
* */
fun getUser() : User
= User(
"Thiengo",
"thiengocalopsita@gmail.com",
R.drawable.lamborghini_urus )
}
}

 

Note que todos os recursos estáticos do projeto se encontrão no /res do GitHub dele.

Classe utilitária

No pacote /util teremos uma classe utilitária com métodos que não se enquadram em outras classes do projeto ou que são utilizados em mais de um ponto do aplicativo.

Segue classe Util:

class Util {
companion object {

/*
* Defini a cor de background do ImageView, que na verdade será
* útil para que a border com radius não tenha contraste com as
* cores laterais da imagem.
* */
fun setImageViewBgColor( context: Context, imageView: ImageView ) {
val bitmap = (imageView.getDrawable() as BitmapDrawable).bitmap

/*
* Estamos obtendo a cor do pixel na coordenada (2.1dp, 2.1dp),
* pois sabemos que nesta coordenada se inicia a verdadeira
* cor de borda da imagem original, isso tendo em mente
* que o shape image_view_sneaker.xml tem um padding de
* 2dp e um background branco e não queremos obtê-lo para a
* definição dinâmica de imagem de background.
* */
val initPixelPosition = Util.convertDpToPixel( context.resources, 2.1F )
val pixel = bitmap.getPixel( initPixelPosition, initPixelPosition )

val bgShape = imageView.background.current as GradientDrawable
bgShape.setColor( pixel )
}

fun convertDpToPixel( resources: Resources, dp: Float ): Int {
val metrics = resources.getDisplayMetrics()
val px = dp * (metrics.densityDpi / 160f)
return Math.round( px )
}

/*
* Apresenta ou esconde os ImageViews de gênero de acordo com o perfil
* do tênis em teste.
* */
fun setGenre( sneaker: Sneaker, male: ImageView, female: ImageView ){
male.visibility =
if( sneaker.isForMale )
View.VISIBLE
else
View.GONE

female.visibility =
if( sneaker.isForFemale )
View.VISIBLE
else
View.GONE
}

/*
* Coloca estrela cheia ou vazia no ImageView de rating de tênis.
* */
fun setStar( parent: View, starResourceId: Int, position: Int, rating: Int ){
val ivStar = parent.findViewById( starResourceId ) as ImageView

ivStar.setImageResource(
if( position <= rating )
R.drawable.ic_star_black_18dp
else
R.drawable.ic_star_border_white_18dp
)
}
}
}

 

Alguns métodos da classe utilitária serão melhor entendidos por você assim que construirmos os códigos das atividades e da classe adaptadora.

Classe adaptadora

Para a classe adaptadora de objetos Sneaker, carregada junto ao RecyclerView da atividade principal, vamos iniciar com o layout de item, /res/layout/sneaker.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="wrap_content"
android:layout_marginBottom="7dp"
android:layout_marginLeft="14dp"
android:layout_marginRight="14dp"
android:layout_marginTop="7dp"
android:background="@drawable/item_border_and_background"
android:padding="6dp"
tools:context="thiengo.com.br.tnismobshop.SneakersActivity">

<ImageView
android:id="@+id/iv_sneaker"
android:layout_width="81dp"
android:layout_height="81dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:background="@drawable/image_view_sneaker"
android:scaleType="fitCenter" />

<ImageView
android:id="@+id/iv_sneaker_gallery_01"
style="@style/AppTheme.ItemImgGallery"
android:layout_alignTop="@+id/iv_sneaker"
android:contentDescription="@string/desc_sneaker_first_gallery_image" />

<ImageView
android:id="@+id/iv_sneaker_gallery_02"
style="@style/AppTheme.ItemImgGallery"
android:layout_below="@+id/iv_sneaker_gallery_01"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/desc_sneaker_second_gallery_image" />

<ImageView
android:id="@+id/iv_sneaker_gallery_03"
style="@style/AppTheme.ItemImgGallery"
android:layout_below="@+id/iv_sneaker_gallery_02"
android:contentDescription="@string/desc_sneaker_third_gallery_image" />

<View
android:id="@+id/vv_vertical_line"
android:layout_width="1dp"
android:layout_height="81dp"
android:layout_alignParentTop="true"
android:layout_marginEnd="6dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginStart="6dp"
android:layout_toEndOf="@+id/iv_sneaker_gallery_01"
android:layout_toRightOf="@+id/iv_sneaker_gallery_01"
android:background="@android:color/white" />

<TextView
android:id="@+id/tv_model"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/tv_free_delivery"
android:layout_alignParentTop="true"
android:layout_alignRight="@+id/tv_free_delivery"
android:layout_toEndOf="@id/vv_vertical_line"
android:layout_toRightOf="@id/vv_vertical_line"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />

<ImageView
android:id="@+id/iv_brand"
android:layout_width="wrap_content"
android:layout_height="14dp"
android:layout_below="@+id/tv_model"
android:layout_marginBottom="4dp"
android:layout_toEndOf="@id/vv_vertical_line"
android:layout_toRightOf="@id/vv_vertical_line"
android:contentDescription="@string/desc_sneaker_brand"
android:scaleType="fitCenter"
android:tint="@color/colorPrimaryLight" />

<ImageView
android:id="@+id/iv_genre_male"
style="@style/AppTheme.ItemIconGenre"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:layout_toEndOf="@id/vv_vertical_line"
android:layout_toRightOf="@id/vv_vertical_line"
android:contentDescription="@string/desc_male_icon"
android:src="@drawable/ic_gender_male_white_18dp" />

<ImageView
android:id="@+id/iv_genre_female"
style="@style/AppTheme.ItemIconGenre"
android:layout_toEndOf="@id/iv_genre_male"
android:layout_toRightOf="@id/iv_genre_male"
android:contentDescription="@string/desc_female_icon"
android:src="@drawable/ic_gender_female_white_18dp" />

<ImageView
android:id="@+id/iv_rating_star_01"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_marginLeft="0dp"
android:layout_marginStart="0dp"
android:layout_toEndOf="@id/vv_vertical_line"
android:layout_toRightOf="@id/vv_vertical_line" />

<ImageView
android:id="@+id/iv_rating_star_02"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/iv_rating_star_01"
android:layout_toRightOf="@id/iv_rating_star_01" />

<ImageView
android:id="@+id/iv_rating_star_03"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/iv_rating_star_02"
android:layout_toRightOf="@id/iv_rating_star_02" />

<ImageView
android:id="@+id/iv_rating_star_04"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/iv_rating_star_03"
android:layout_toRightOf="@id/iv_rating_star_03" />

<ImageView
android:id="@+id/iv_rating_star_05"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/iv_rating_star_04"
android:layout_toRightOf="@id/iv_rating_star_04" />

<TextView
android:id="@+id/tv_rating_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginLeft="1dp"
android:layout_marginStart="1dp"
android:layout_toEndOf="@id/iv_rating_star_05"
android:layout_toRightOf="@id/iv_rating_star_05"
android:textColor="@android:color/white"
android:textSize="13sp" />

<TextView
android:id="@+id/tv_free_delivery"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:background="@drawable/text_view_free_delivery_background"
android:gravity="end"
android:text="@string/label_free"
android:textColor="@android:color/white" />

<TextView
android:id="@+id/tv_put_shop_cart"
android:layout_width="wrap_content"
android:layout_height="22dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_free_delivery"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:background="@drawable/text_view_shop_cart_background"
android:gravity="start"
android:text="@string/label_put_on_shop_cart"
android:textColor="@android:color/white"
android:textSize="12sp" />

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_put_shop_cart"
android:background="@drawable/dark_green_background"
android:gravity="end"
android:orientation="vertical"
android:paddingBottom="1dp"
android:paddingLeft="4dp"
android:paddingRight="4dp">

<TextView
android:id="@+id/tv_price_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="13sp"
android:textStyle="bold|italic" />

<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:textColor="@android:color/white"
android:textSize="9sp"
android:textStyle="bold|italic" />
</LinearLayout>
</RelativeLayout>

 

Antes da apresentação do diagrama do layout, vamos a alguns arquivos drawables referenciados em layout.

Primeiro o arquivo de borda de cada item, /res/drawable/item_border_and_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--
Cor de background da View.
-->
<solid android:color="@android:color/transparent" />

<!--
Curvatura das pontas da View retangular.
-->
<corners android:radius="2dp" />

<!--
Largura e cor da borda.
-->
<stroke
android:width="1dp"
android:color="@android:color/white" />
</shape>

 

Com ele conseguimos o seguinte resultado:

Bordar de item via drawable

Agora o arquivo de background e borda de cada imagem do tênis presente em item, /res/drawable/image_view_sneaker.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--
Cor de background da View.
-->
<solid android:color="@android:color/white" />

<!--
Espaçamentos interno aplicados a View.
-->
<padding
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:top="2dp" />

<!--
Curvatura das pontas da View retangular.
-->
<corners android:radius="2dp" />
</shape>

 

Com o arquivo acima temos o seguinte resultado:

Bordas arredondadas em imagens

Agora o arquivo de background da área de "entrega" do item, o arquivo /res/drawable/text_view_free_delivery_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

<!--
Item de definição de background de shape.
-->
<item>
<!--
Definindo cor de background; curvatura de pontas;
e espaçamento interno.
-->
<shape android:shape="rectangle">
<solid android:color="@color/colorPrimaryDark" />
<corners android:radius="2dp" />
<padding
android:bottom="4dp"
android:left="4dp"
android:right="6dp"
android:top="2dp" />
</shape>
</item>

<!--
Item de definição de imagem de background, posicionada
a 39dp à direita da View.
-->
<item
android:width="22dp"
android:height="20dp"
android:bottom="0dp"
android:drawable="@drawable/ic_truck_white_24dp"
android:left="0dp"
android:right="39dp"
android:top="0dp" />
</layer-list>

 

E então o resultado obtido com este arquivo drawable:

Logo de entrega grátis

Assim o arquivo de background da área de carrinho de compra do item, /res/drawable/text_view_shop_cart_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

<!--
Item de definição de background de shape.
-->
<item>
<!--
Definindo cor de background; curvatura de pontas;
e espaçamento interno.
-->
<shape android:shape="rectangle">
<solid android:color="@color/colorAccentDark" />
<corners android:radius="2dp" />
<padding
android:bottom="4dp"
android:left="4dp"
android:right="6dp"
android:top="2dp" />
</shape>
</item>

<!--
Item de definição de imagem de background, posicionada
a 63dp à esquerda da View.
-->
<item
android:width="17dp"
android:height="16dp"
android:bottom="0dp"
android:left="63dp"
android:right="0dp"
android:top="0dp">
<!--
Definindo a imagem e a cor dela.
-->
<bitmap
android:src="@drawable/ic_shopping_cart_black_18dp"
android:tint="@color/colorPrimaryLight" />
</item>
</layer-list>

 

A seguir o resultado que temos quando utilizando o drawable anterior em layout:

Botão de carrinho de compra

Agora o arquivo de background para também o destaque do preço de tênis no item, /res/drawable/dark_green_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--
Cor de background da View.
-->
<solid android:color="@color/colorPrimaryDark" />

<!--
Curvatura das pontas da View retangular.
-->
<corners android:radius="2dp" />
</shape>

 

Abaixo o resultado obtido quando o drawable acima é utilizado no layout de item:

Rótulo de preço de tênis

Agora o diagrama do layout sneaker.xml:

Diagrama do layout sneaker.xml

Por fim o código Kotlin da classe SneakersAdapter:

class SneakersAdapter(
private val context: Context,
private val sneakers: List<Sneaker> ) :
RecyclerView.Adapter<SneakersAdapter.ViewHolder>() {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int ) : SneakersAdapter.ViewHolder {

val v = LayoutInflater
.from( context )
.inflate( R.layout.sneaker, parent, false )

return ViewHolder( v )
}

override fun onBindViewHolder(
holder: ViewHolder,
position: Int ) {

holder.setData( sneakers[ position ] )
}

override fun getItemCount(): Int {
return sneakers.size
}

inner class ViewHolder( itemView: View ) :
RecyclerView.ViewHolder( itemView ),
View.OnClickListener {

val ivSneaker: ImageView
val ivSneakerGallery01: ImageView
val ivSneakerGallery02: ImageView
val ivSneakerGallery03: ImageView
val tvModel: TextView
val ivBrand: ImageView
val ivGenreMale: ImageView
val ivGenreFemale: ImageView
val tvPriceParcels: TextView
val tvPrice: TextView

init {
itemView.setOnClickListener( this )

ivSneaker = itemView.findViewById( R.id.iv_sneaker )
ivSneakerGallery01 = itemView.findViewById( R.id.iv_sneaker_gallery_01 )
ivSneakerGallery02 = itemView.findViewById( R.id.iv_sneaker_gallery_02 )
ivSneakerGallery03 = itemView.findViewById( R.id.iv_sneaker_gallery_03 )
tvModel = itemView.findViewById( R.id.tv_model )
ivBrand = itemView.findViewById( R.id.iv_brand )
ivGenreMale = itemView.findViewById( R.id.iv_genre_male )
ivGenreFemale = itemView.findViewById( R.id.iv_genre_female )
tvPriceParcels = itemView.findViewById( R.id.tv_price_parcels )
tvPrice = itemView.findViewById( R.id.tv_price )
}

fun setData( sneaker: Sneaker ) {
setGallery( sneaker )

tvModel.text = sneaker.model

ivBrand.setImageResource( sneaker.brand.imageResource )
ivBrand.contentDescription = sneaker.brand.name

Util.setGenre( sneaker, ivGenreMale, ivGenreFemale )
setRating( sneaker )
setPrice( sneaker )
}

private fun setGallery(sneaker: Sneaker){
/*
* Imagem principal do tênis.
* */
ivSneaker.setImageResource( sneaker.imageResource )
ivSneaker.contentDescription = String
.format( "%s %s", context.getString( R.string.sneacker_label ), sneaker.model )

ivSneakerGallery01.setImageResource( sneaker.album[ 0 ] )
ivSneakerGallery02.setImageResource( sneaker.album[ 1 ] )
ivSneakerGallery03.setImageResource( sneaker.album[ 2 ] )

/*
* Atualizando a cor de background das imagens para que
* a borda fique sem contraste.
* */
Util.setImageViewBgColor( context, ivSneaker )
Util.setImageViewBgColor( context, ivSneakerGallery01 )
Util.setImageViewBgColor( context, ivSneakerGallery02 )
Util.setImageViewBgColor( context, ivSneakerGallery03 )
}

/*
* Coloca os resources corretos de estrelas de acordo com os
* números das avaliações dos tênis.
* */
private fun setRating( sneaker: Sneaker ){
val tvRatingAmount = itemView
.findViewById( R.id.tv_rating_amount ) as TextView

tvRatingAmount.text = String.format( "%s", sneaker.rating.amount )

Util.setStar( itemView, R.id.iv_rating_star_01, 1, sneaker.rating.stars )
Util.setStar( itemView, R.id.iv_rating_star_02, 2, sneaker.rating.stars )
Util.setStar( itemView, R.id.iv_rating_star_03, 3, sneaker.rating.stars )
Util.setStar( itemView, R.id.iv_rating_star_04, 4, sneaker.rating.stars )
Util.setStar( itemView, R.id.iv_rating_star_05, 5, sneaker.rating.stars )
}

private fun setPrice( sneaker: Sneaker ){
tvPriceParcels.text = sneaker.getPriceParcelsAsString()
tvPrice.text = sneaker.getPriceAsString()
}

override fun onClick( view: View ) {
val intent = Intent( context, SneakerDetailsActivity::class.java )

intent.putExtra( Sneaker.KEY, sneakers[ adapterPosition ] )
context.startActivity( intent )
}
}
}

 

Está classe é uma das que mais se beneficiará com o uso da Data Binding API.

Atividade principal

A SneakersActivity é simples, o maior trecho de código é boilerplate devido ao uso de menu gaveta.

Vamos iniciar com o arquivo de layout de conteúdo, /res/layout/app_bar_sneakers.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SneakersActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
android:id="@+id/rv_sneakers"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:paddingTop="7dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>

 

A seguir o diagrama do layout anterior:

Diagrama do layout app_bar_sneakers.xml

Agora o arquivo XML de cabeçalho do menu gaveta, /res/layout/nav_header_sneaker.xml:

<?xml version="1.0" encoding="utf-8"?>
<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="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar"
android:gravity="bottom"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark">

<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:scaleType="centerCrop"
app:riv_border_color="@color/colorPrimaryDark"
app:riv_border_width="2dp"
app:riv_corner_radius="5dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />

<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

<TextView
android:id="@+id/tv_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

 

Então o diagrama do layout de cabeçalho:

Diagrama do layout nav_header_sneaker.xml

Assim o arquivo de menu com as opções do menu gaveta, arquivo /res/menu/activity_tenis_list_drawer.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">

<group android:checkableBehavior="single">
<item
android:id="@+id/nav_camera"
android:checked="true"
android:icon="@drawable/ic_shoe_formal_white_24dp"
android:title="@string/nav_item_all_brands" />
<item
android:id="@+id/nav_gallery"
android:icon="@drawable/ic_human_male_white_24dp"
android:title="@string/nav_item_male" />
<item
android:id="@+id/nav_slideshow"
android:icon="@drawable/ic_human_female_white_24dp"
android:title="@string/nav_item_female" />
<item
android:id="@+id/nav_manage"
android:icon="@drawable/ic_account_child_white_24dp"
android:title="@string/nav_item_child" />
</group>

<item android:title="@string/nav_group_config">
<menu>
<item
android:id="@+id/nav_share"
android:icon="@drawable/ic_cellphone_android_white_24dp"
android:title="@string/nav_item_app" />
<item
android:id="@+id/nav_send"
android:icon="@drawable/ic_wrench_white_24dp"
android:title="@string/nav_item_account" />
</menu>
</item>
</menu>

 

Abaixo o diagrama do menu anterior:

Diagrama do menu activity_tenis_list_drawer.xml

Por fim o layout principal, que referencia todos os outros desta seção, /res/layout/sneakers_activity.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
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"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">

<include
layout="@layout/app_bar_sneakers"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/colorBgNavigationView"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_sneaker"
app:menu="@menu/activity_tenis_list_drawer" />
</android.support.v4.widget.DrawerLayout>

 

Abaixo o diagrama do layout principal:

Diagrama do layout sneakers_activity.xml

Então o código Kotlin da atividade principal do projeto:

class SneakersActivity :
AppCompatActivity(),
NavigationView.OnNavigationItemSelectedListener {

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_sneakers )
setSupportActionBar( toolbar )

val toggle = ActionBarDrawerToggle(
this,
drawer_layout,
toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close
)
drawer_layout.addDrawerListener( toggle )
toggle.syncState()

nav_view.setNavigationItemSelectedListener( this )

initHeader()
initSneakers()
}

private fun initSneakers(){
rv_sneakers.setHasFixedSize( true )

val layoutManager = LinearLayoutManager( this )
rv_sneakers.layoutManager = layoutManager

rv_sneakers.adapter = SneakersAdapter(
this,
Database.getSneakers( this )
)
}

private fun initHeader(){
val user = Database.getUser()
val viewRoot = nav_view.getHeaderView(0) as ViewGroup

val ivProfile = viewRoot.getChildAt(0) as ImageView
val tvName = viewRoot.getChildAt(1) as TextView
val tvEmail = viewRoot.getChildAt(2) as TextView

ivProfile.setImageResource( user.image )
tvName.text = user.name
tvEmail.text = user.email
}

override fun onBackPressed() {
if ( drawer_layout.isDrawerOpen( GravityCompat.START ) ) {
drawer_layout.closeDrawer( GravityCompat.START )
}
else {
super.onBackPressed()
}
}

/*
* Somente para apresentarmos os itens de "buscar" e
* "carrinho de compras"
* */
override fun onCreateOptionsMenu( menu: Menu ): Boolean {
menuInflater.inflate( R.menu.tenis_list, menu )
return true
}

override fun onNavigationItemSelected( item: MenuItem ): Boolean {
drawer_layout.closeDrawer( GravityCompat.START )

/*
* Para este aplicativo de exemplo, manterá sempre
* o mesmo item selecionado.
* */
return false
}
}

Atividade de detalhes de tênis

Como fizemos nas seções anteriores, aqui vamos começar com as apresentações dos layouts. Primeiro o de conteúdo, /res/layout/content_sneaker_details.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="thiengo.com.br.tnismobshop.SneakerDetailsActivity"
tools:showIn="@layout/activity_sneaker_details">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="14dp">

<ImageView
android:id="@+id/iv_brand"
android:layout_width="40dp"
android:layout_height="20dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:contentDescription="@string/desc_sneaker_brand"
android:scaleType="fitCenter"
android:tint="@color/colorPrimaryLight" />

<View
android:id="@+id/vv_horizontal_line_01"
android:layout_width="40dp"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_brand"
android:layout_marginBottom="3dp"
android:layout_marginTop="14dp"
android:background="@android:color/white" />

<ImageView
android:id="@+id/iv_genre_male"
style="@style/AppTheme.DetailsIconGenre"
android:layout_below="@+id/vv_horizontal_line_01"
android:contentDescription="@string/desc_male_icon"
android:src="@drawable/ic_gender_male_white_18dp" />

<ImageView
android:id="@+id/iv_genre_female"
style="@style/AppTheme.DetailsIconGenre"
android:layout_below="@+id/iv_genre_male"
android:contentDescription="@string/desc_female_icon"
android:src="@drawable/ic_gender_female_white_18dp" />

<View
android:id="@+id/vv_horizontal_line_02"
android:layout_width="40dp"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_genre_female"
android:layout_marginBottom="14dp"
android:layout_marginTop="3dp"
android:background="@android:color/white" />

<Spinner
android:id="@+id/sp_cor"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_02"
android:background="@drawable/spinner_mini_border_and_background"
android:entries="@array/colors"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:popupBackground="@color/colorPrimary"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme" />

<View
android:id="@+id/vv_horizontal_line_03"
android:layout_width="40dp"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/sp_cor"
android:layout_marginBottom="14dp"
android:layout_marginTop="14dp"
android:background="@android:color/white" />

<Spinner
android:id="@+id/sp_size"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_03"
android:background="@drawable/spinner_mini_border_and_background"
android:entries="@array/sizes"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:popupBackground="@color/colorPrimary"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme" />

<View
android:id="@+id/vv_vertical_line_01"
style="@style/AppTheme.DetailsVerticalLine"
android:layout_toEndOf="@+id/iv_brand"
android:layout_toRightOf="@+id/iv_brand" />

<LinearLayout
android:id="@+id/ll_center_and_right"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/vv_vertical_line_01"
android:layout_toRightOf="@+id/vv_vertical_line_01"
android:orientation="horizontal">

<ImageView
android:id="@+id/iv_sneaker_01"
android:layout_width="0dp"
android:layout_height="240dp"
android:layout_weight="1"
android:background="@drawable/image_view_sneaker"
android:scaleType="fitCenter" />

<View
android:id="@+id/vv_vertical_line_02"
style="@style/AppTheme.DetailsVerticalLine" />

<LinearLayout
android:id="@+id/ll_gallery"
android:layout_width="68dp"
android:layout_height="240dp"
android:orientation="vertical">

<ImageView
android:id="@+id/iv_sneaker_02"
style="@style/AppTheme.DetailsImgGallery" />

<ImageView
android:id="@+id/iv_sneaker_03"
style="@style/AppTheme.DetailsImgGallery"
android:layout_marginBottom="14dp"
android:layout_marginTop="14dp" />

<ImageView
android:id="@+id/iv_sneaker_04"
style="@style/AppTheme.DetailsImgGallery" />

</LinearLayout>
</LinearLayout>

<RelativeLayout
android:id="@+id/rl_buy_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/ll_center_and_right"
android:layout_marginTop="14dp"
android:background="@drawable/dark_green_background"
android:padding="6dp">

<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:textColor="@android:color/white"
android:textSize="17sp"
android:textStyle="bold|italic" />

<TextView
android:id="@+id/tv_price_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_price"
android:layout_marginTop="1dp"
android:textColor="@android:color/white"
android:textSize="17sp"
android:textStyle="bold|italic" />

<View
android:id="@+id/vv_vertical_line_03"
style="@style/AppTheme.DetailsSmalVerticalLine"
android:layout_alignTop="@+id/tv_price"
android:layout_toEndOf="@+id/tv_price"
android:layout_toRightOf="@+id/tv_price" />

<TextView
android:id="@+id/tv_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/vv_vertical_line_04"
android:layout_alignLeft="@+id/vv_vertical_line_03"
android:layout_alignParentTop="true"
android:layout_alignRight="@+id/vv_vertical_line_04"
android:layout_alignStart="@+id/vv_vertical_line_03"
android:layout_below="@+id/tv_price"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="1dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@android:color/white"
android:textSize="9sp" />

<Spinner
android:id="@+id/sp_amount"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_alignEnd="@+id/vv_vertical_line_04"
android:layout_alignLeft="@+id/vv_vertical_line_03"
android:layout_alignRight="@+id/vv_vertical_line_04"
android:layout_alignStart="@+id/vv_vertical_line_03"
android:layout_below="@+id/tv_amount"
android:layout_gravity="center"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:background="@drawable/spinner_mini_border_and_background"
android:entries="@array/amount"
android:gravity="center"
android:paddingBottom="2dp"
android:paddingTop="2dp"
android:popupBackground="@color/colorPrimary"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme" />

<View
android:id="@+id/vv_vertical_line_04"
style="@style/AppTheme.DetailsSmalVerticalLine"
android:layout_alignTop="@+id/tv_price"
android:layout_toLeftOf="@+id/bt_buy"
android:layout_toStartOf="@+id/bt_buy" />

<Button
android:id="@+id/bt_buy"
style="@style/AppTheme.DetailsButton"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="@string/label_buy" />
</RelativeLayout>

<TextView
android:id="@+id/tv_free_delivery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/rl_buy_data"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:text="@string/brazil_free_delivery"
android:textColor="@color/colorFreeDelivery"
android:textSize="12sp" />

<ImageView
android:layout_width="16dp"
android:layout_height="15dp"
android:layout_alignTop="@+id/tv_free_delivery"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:layout_marginTop="1dp"
android:layout_toLeftOf="@+id/tv_free_delivery"
android:layout_toStartOf="@+id/tv_free_delivery"
android:contentDescription="@string/desc_truck_delivery"
android:src="@drawable/ic_truck_white_24dp" />

<View
android:id="@+id/vv_horizontal_line_04"
style="@style/AppTheme.DetailsHorizontalLine"
android:layout_below="@+id/tv_free_delivery" />

<TextView
android:id="@+id/tv_label_recommended"
style="@style/AppTheme.DetailsContentLabel"
android:layout_below="@+id/vv_horizontal_line_04"
android:text="@string/label_recommended" />

<TextView
android:id="@+id/tv_recommended"
style="@style/AppTheme.DetailsContent"
android:layout_alignTop="@+id/tv_label_recommended"
android:layout_toEndOf="@+id/tv_label_recommended"
android:layout_toRightOf="@+id/tv_label_recommended" />

<TextView
android:id="@+id/tv_label_type"
style="@style/AppTheme.DetailsContentLabel"
android:layout_below="@+id/tv_label_recommended"
android:text="@string/label_type" />

<TextView
android:id="@+id/tv_type"
style="@style/AppTheme.DetailsContent"
android:layout_alignTop="@+id/tv_label_type"
android:layout_toEndOf="@+id/tv_label_type"
android:layout_toRightOf="@+id/tv_label_type" />

<TextView
android:id="@+id/tv_label_composition"
style="@style/AppTheme.DetailsContentLabel"
android:layout_below="@+id/tv_label_type"
android:text="@string/label_composition" />

<TextView
android:id="@+id/tv_composition"
style="@style/AppTheme.DetailsContent"
android:layout_alignTop="@+id/tv_label_composition"
android:layout_toEndOf="@+id/tv_label_composition"
android:layout_toRightOf="@+id/tv_label_composition" />

<TextView
android:id="@+id/tv_label_desc"
style="@style/AppTheme.DetailsContentLabel"
android:layout_below="@+id/tv_label_composition"
android:text="@string/label_description" />

<TextView
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/tv_label_desc"
android:layout_toEndOf="@+id/tv_label_desc"
android:layout_toRightOf="@+id/tv_label_desc"
android:textColor="@android:color/white" />

<View
android:id="@+id/vv_horizontal_line_05"
style="@style/AppTheme.DetailsHorizontalLine"
android:layout_below="@+id/tv_desc"
android:layout_marginBottom="14dp"
android:layout_marginTop="9dp" />

<LinearLayout
android:id="@+id/rl_rating"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/vv_horizontal_line_05"
android:background="@drawable/dark_green_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="6dp">

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingEnd="20dp"
android:paddingLeft="14dp"
android:paddingRight="20dp"
android:paddingStart="14dp">

<ImageView
android:id="@+id/iv_rating_star_01"
style="@style/AppTheme.DetailsStar"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="0dp"
android:layout_marginStart="0dp" />

<ImageView
android:id="@+id/iv_rating_star_02"
style="@style/AppTheme.DetailsStar"
android:layout_toEndOf="@id/iv_rating_star_01"
android:layout_toRightOf="@id/iv_rating_star_01" />

<ImageView
android:id="@+id/iv_rating_star_03"
style="@style/AppTheme.DetailsStar"
android:layout_toEndOf="@id/iv_rating_star_02"
android:layout_toRightOf="@id/iv_rating_star_02" />

<ImageView
android:id="@+id/iv_rating_star_04"
style="@style/AppTheme.DetailsStar"
android:layout_toEndOf="@id/iv_rating_star_03"
android:layout_toRightOf="@id/iv_rating_star_03" />

<ImageView
android:id="@+id/iv_rating_star_05"
style="@style/AppTheme.DetailsStar"
android:layout_toEndOf="@id/iv_rating_star_04"
android:layout_toRightOf="@id/iv_rating_star_04" />

<TextView
android:id="@+id/tv_rating_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginLeft="2dp"
android:layout_marginStart="2dp"
android:layout_marginTop="-2dp"
android:layout_toEndOf="@id/iv_rating_star_05"
android:layout_toRightOf="@id/iv_rating_star_05"
android:textColor="@android:color/white" />
</RelativeLayout>

<Button
style="@style/AppTheme.DetailsButton"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/label_access_rating" />
</LinearLayout>
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>

 

Agora vamos aos arquivos drawables, ainda não apresentados em artigo e presentes no layout anterior.

Iniciando pelo drawable de configuração dos pequenos Spinners presentes no design, o arquivo /res/drawable/spinner_mini_border_and_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

<!--
Item de definição de background de shape.
-->
<item>

<!--
Definindo cor de background; curvatura de pontas;
e largura e cor de borda.
-->
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="2dp" />
<stroke
android:width="1dp"
android:color="@android:color/white" />
</shape>
</item>

<!--
Item de definição de imagem de background, posicionada
a 0.5dp à direita da View.
-->
<item android:right="0.5dp">
<!--
Definindo a imagem e o posicionamento dentro da View.
-->
<bitmap
android:gravity="center_vertical|right"
android:src="@drawable/ic_keyboard_arrow_down_white_18dp" />
</item>
</layer-list>

 

A seguir um print de como o drawable acima deixa o Spinner:

Spinners com bordas brancas e arredondadas

Agora o arquivo drawable dos botões azuis em tela, o arquivo /res/layout/button_blue_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--
Cor de background da View.
-->
<solid android:color="@color/colorAccent" />

<!--
Curvatura das pontas da View retangular.
-->
<corners android:radius="2dp" />
</shape>

 

Então o resultado do uso do drawable acima:

Botão azul de compra

Abaixo o diagrama do layout content_sneaker_details.xml:

Diagrama do layout content_sneaker_details.xml

Então o layout principal da atividade de detalhes de tênis, /res/layout/activity_sneaker_details.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context="thiengo.com.br.tnismobshop.SneakerDetailsActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_sneaker_details" />

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@drawable/ic_share_white_24dp" />
</android.support.design.widget.CoordinatorLayout>

 

A seguir o diagrama do layout anterior:

Diagrama do layout activity_sneaker_details.xml

Antes de apresentarmos o código dinâmico da atividade SneakerDetails, vamos ao layout da caixa de diálogo de pagamento que também é acionada nesta atividade. Segue código XML do arquivo /res/layout/dialog_payment.xml:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fillViewport="true">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp">

<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/dialog_title"
android:textColor="@color/colorDarkDialog"
android:textSize="25sp" />

<View
android:id="@+id/vv_horizontal_line_01"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_title"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:background="@color/colorDialogLine" />

<ImageView
android:id="@+id/iv_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_01"
android:contentDescription="@string/desc_credit_cards"
android:scaleType="fitCenter"
android:src="@drawable/credit_cards" />

<View
android:id="@+id/vv_horizontal_line_02"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_cards"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:background="@color/colorDialogLine" />

<TextView
android:id="@+id/tv_card_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_02"
android:text="@string/dialog_label_card_number"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<EditText
android:id="@+id/et_card_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_card_number"
android:layout_toLeftOf="@+id/et_card_cvv"
android:layout_toStartOf="@+id/et_card_cvv"
android:background="@drawable/field_border_and_background_dark"
android:hint="@string/dialog_hint_card_number"
android:inputType="number"
android:padding="10dp"
android:textColor="@color/colorDarkDialog" />

<TextView
android:id="@+id/tv_card_cvv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/et_card_cvv"
android:layout_alignStart="@+id/et_card_cvv"
android:layout_below="@+id/vv_horizontal_line_02"
android:text="@string/dialog_label_card_cvv"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<EditText
android:id="@+id/et_card_cvv"
android:layout_width="140dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_card_cvv"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:background="@drawable/field_border_and_background_dark"
android:hint="@string/dialog_hint_card_cvv"
android:inputType="number"
android:padding="10dp"
android:textColor="@color/colorDarkDialog" />

<TextView
android:id="@+id/tv_card_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/et_card_number"
android:layout_marginTop="12dp"
android:text="@string/dialog_label_card_owner"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<EditText
android:id="@+id/et_card_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_card_name"
android:background="@drawable/field_border_and_background_dark"
android:inputType="textPersonName"
android:padding="10dp"
android:textColor="@color/colorDarkDialog" />

<TextView
android:id="@+id/tv_card_expired"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/et_card_name"
android:layout_marginTop="12dp"
android:text="@string/dialog_label_card_expire"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_card_expired_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_card_expired"
android:layout_marginTop="-2dp"
android:text="@string/dialog_label_card_expire_month"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<Spinner
android:id="@+id/sp_card_expired_month"
android:layout_width="44dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_card_expired_month"
android:background="@drawable/spinner_mini_border_and_background_dark"
android:entries="@array/months"
android:paddingBottom="12dp"
android:paddingTop="12dp"
android:popupBackground="@color/colorDialogPopUpSpinner"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerThemeDark" />

<TextView
android:id="@+id/tv_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/sp_card_expired_month"
android:layout_marginEnd="6dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginStart="6dp"
android:layout_marginTop="7dp"
android:layout_toEndOf="@+id/sp_card_expired_month"
android:layout_toRightOf="@+id/sp_card_expired_month"
android:text="@string/dialog_label_card_expire_bar"
android:textColor="@color/colorDarkDialog"
android:textSize="20sp" />

<TextView
android:id="@+id/tv_card_expired_year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/sp_card_expired_year"
android:layout_alignStart="@+id/sp_card_expired_year"
android:layout_alignTop="@+id/tv_card_expired_month"
android:text="@string/dialog_label_card_expire_year"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<Spinner
android:id="@+id/sp_card_expired_year"
android:layout_width="61dp"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_card_expired_year"
android:layout_toEndOf="@+id/tv_bar"
android:layout_toRightOf="@+id/tv_bar"
android:background="@drawable/spinner_mini_border_and_background_dark"
android:entries="@array/years"
android:paddingBottom="12dp"
android:paddingTop="12dp"
android:popupBackground="@color/colorDialogPopUpSpinner"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerThemeDark" />

<View
android:id="@+id/vv_vertical_line_01"
android:layout_width="1dp"
android:layout_height="68dp"
android:layout_alignTop="@+id/tv_card_expired"
android:layout_marginBottom="6dp"
android:layout_marginEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:layout_toEndOf="@+id/sp_card_expired_year"
android:layout_toRightOf="@+id/sp_card_expired_year"
android:background="@color/colorDialogLine" />

<TextView
android:id="@+id/tv_buy_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/tv_card_expired"
android:layout_toEndOf="@+id/vv_vertical_line_01"
android:layout_toRightOf="@+id/vv_vertical_line_01"
android:text="@string/dialog_label_card_total_parcels"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<Spinner
android:id="@+id/sp_buy_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_buy_parcels"
android:layout_toEndOf="@+id/vv_vertical_line_01"
android:layout_toRightOf="@+id/vv_vertical_line_01"
android:background="@drawable/spinner_mini_border_and_background_dark"
android:entries="@array/parcels"
android:paddingBottom="12dp"
android:paddingTop="12dp"
android:popupBackground="@color/colorDialogPopUpSpinner"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerThemeDark" />

<Button
android:id="@+id/bt_finish_buy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/vv_vertical_line_01"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignStart="@+id/vv_vertical_line_01"
android:layout_below="@+id/sp_buy_parcels"
android:layout_marginTop="12dp"
android:background="@drawable/button_blue_background"
android:text="@string/label_bt_finish_buy"
android:textAllCaps="false"
android:textColor="@android:color/white" />

<TextView
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignTop="@+id/bt_finish_buy"
android:text="@string/dialog_label_total"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_total_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_total"
android:layout_marginTop="-4dp"
android:textColor="@color/colorDarkDialog"
android:textSize="21sp"
android:textStyle="bold|italic" />

<TextView
android:id="@+id/tv_total_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_total_price"
android:layout_marginTop="-4dp"
android:text="@string/dialog_label_parcels_without_taxes"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />
</RelativeLayout>
</ScrollView>

 

Agora os arquivos drawables referenciados no layout acima e ainda não apresentados em artigo. Começando pelo arquivo para campos de formulário, EditTexts, /res/drawable/field_border_and_background_dark.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--
Cor de background da View.
-->
<solid android:color="@android:color/transparent" />

<!--
Curvatura das pontas da View retangular.
-->
<corners android:radius="2dp" />

<!--
Largura e cor da borda.
-->
<stroke
android:width="0.8dp"
android:color="@color/colorDarkDialog" />
</shape>

 

A seguir o resultado do uso do drawable anterior:

EditText com bordas arredondadas

E então o drawable para os Spinners da caixa de diálogo, /res/drawable/spinner_mini_border_and_background_dark.xml:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

<!--
Item de definição de background de shape.
-->
<item>
<!--
Definindo cor de background; curvatura de pontas;
e largura e cor de borda.
-->
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="2dp" />
<stroke
android:width="0.8dp"
android:color="@color/colorDarkDialog" />
</shape>
</item>

<!--
Item de definição de imagem de background, posicionada
a 0.5dp à direita da View.
-->
<item android:right="0.5dp">
<!--
Definindo a imagem; a cor dela; e o posicionamento
dentro da View.
-->
<bitmap
android:tint="@color/colorDarkDialog"
android:gravity="center_vertical|right"
android:src="@drawable/ic_keyboard_arrow_down_white_18dp" />
</item>
</layer-list>

 

Abaixo o resultado da utilização do drawable anterior:

Spinners com bordas arredondadas e escuras

Agora o diagrama do layout dialog_payment.xml:

Diagrama do layout dialog_payment.xml

Por fim o código Kotlin da atividade de detalhes de tênis:

class SneakerDetailsActivity : AppCompatActivity(),
View.OnClickListener,
AdapterView.OnItemSelectedListener {

lateinit var sneaker: Sneaker
var amount: Int = 1

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_sneaker_details )
setSupportActionBar( toolbar )

fab.setOnClickListener(this)
supportActionBar?.setDisplayHomeAsUpEnabled( true )

/*
* Foi preferível utilizar sneaker como uma propriedade local,
* pois assim evitaremos a necessidade de sempre ter de verificar
* null.
* */
sneaker = intent.getParcelableExtra<Sneaker>( Sneaker.KEY )

/*
* Marca.
* */
iv_brand.setImageResource( sneaker.brand.imageResource )
iv_brand.contentDescription = sneaker.brand.name

/*
* Gênero.
* */
Util.setGenre( sneaker, iv_genre_male, iv_genre_female )

/*
* Galeria de imagens.
* */
iv_sneaker_01.setImageResource( sneaker.imageResource )
iv_sneaker_01.contentDescription = String
.format( "%s %s", getString(R.string.sneaker), sneaker.model )
Util.setImageViewBgColor( this, iv_sneaker_01 )

iv_sneaker_02.setImageResource( sneaker.album[ 0 ] )
Util.setImageViewBgColor( this, iv_sneaker_02 )
iv_sneaker_03.setImageResource( sneaker.album[ 1 ] )
Util.setImageViewBgColor( this, iv_sneaker_03 )
iv_sneaker_04.setImageResource( sneaker.album[ 2 ] )
Util.setImageViewBgColor( this, iv_sneaker_04 )

/*
* Preços, quantidade em estoque e botão comprar.
* */
tv_price.text = String
.format( "%s %s", sneaker.getPriceParcelsAsString(), getString( R.string.or_in ) )
tv_price_parcels.text = String
.format( "%s %s", getString( R.string.until ), sneaker.getPriceParcelsAsString() )
tv_amount.text = sneaker.extraInfo.getStockFormatted( this )
bt_buy.setOnClickListener( this )

sp_amount.setOnItemSelectedListener( this )

/*
* Recomendação, tipo, composição, descrição.
* */
tv_recommended.text = sneaker.extraInfo.recommended
tv_type.text = sneaker.extraInfo.type
tv_composition.text = sneaker.extraInfo.composition
tv_desc.text = sneaker.extraInfo.fullDescription

/* RATING, COLOCANDO AS ESTRELAS CORRETAS */
tv_rating_amount.text = String.format( "%d", sneaker.rating.amount )
Util.setStar( window.decorView, R.id.iv_rating_star_01, 1, sneaker.rating.stars )
Util.setStar( window.decorView, R.id.iv_rating_star_02, 2, sneaker.rating.stars )
Util.setStar( window.decorView, R.id.iv_rating_star_03, 3, sneaker.rating.stars )
Util.setStar( window.decorView, R.id.iv_rating_star_04, 4, sneaker.rating.stars )
Util.setStar( window.decorView, R.id.iv_rating_star_05, 5, sneaker.rating.stars )
}

/*
* Hackcode para que o título da Toolbar seja alterado de
* acordo com o tênis acionado em lista.
* */
override fun onResume() {
super.onResume()
toolbar.title = sneaker.model
}

/*
* Somente para apresentarmos o menu item de "adicionar ao
* carrinho"
* */
override fun onCreateOptionsMenu( menu: Menu? ): Boolean {
val inflater = menuInflater
inflater.inflate( R.menu.menu_sneaker_details, menu )
return true
}

override fun onClick( view: View ) {
/*
* Respondendo ao acionamento do FloatingActionButton.
* */
if( view.id == fab.id ){
Snackbar
.make(
view,
getString( R.string.share_content_shared ),
Snackbar.LENGTH_LONG
)
.setAction( getString( R.string.share_label ), null )
.show()
}

/*
* Respondendo ao acionamento do Button de finalizar
* compra, presente na caixa de diálogo de pagamento.
* */
else if( view.id == R.id.bt_finish_buy ){
buy()
}

/*
* Respondendo ao acionamento do Button "Comprar".
* */
else{
callPaymentDialog()
}
}

/*
* Hackcode para centralizar o item selecionado no Spinner de
* quantidade a comprar de um mesmo modelo de tênis e para obter
* a quantidade em compra escolhida.
* */
override fun onItemSelected(
adapterView: AdapterView<*>,
view: View,
position: Int,
id: Long ) {

(adapterView.getChildAt(0) as TextView).gravity = Gravity.CENTER
amount = position + 1
}
override fun onNothingSelected( adapterView: AdapterView<*>? ) {}

/*
* Abre o dialog de pagamento com cartão de crédito. AlertDialog
* está sendo utilizado por já ser suficiente a essa necessidade
* do aplicativo, mas um DialogFragment poderia ser utilizado
* também.
* */
fun callPaymentDialog() {
val builder = AlertDialog.Builder( this )
val dialogLayout = getLayoutInflater()
.inflate( R.layout.dialog_payment,null )

builder.setView( dialogLayout )

dialogLayout
.findViewById( R.id.tv_total_price )
.text = sneaker.getPriceAsString()

dialogLayout
.findViewById( R.id.bt_finish_buy )
.setOnClickListener( this )

builder.create().show()
}

fun buy() {
val intent = Intent( this, ThankYouActivity::class.java )

intent.putExtra( Sneaker.KEY, sneaker )
intent.putExtra( Sneaker.KEY_AMOUNT, amount )
startActivity( intent )
finish()
}
}

Atividade de "Obrigado pela compra"

A seguir o layout de conteúdo da ThankYouActivity, arquivo XML /res/layout/content_thank_you.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="thiengo.com.br.tnismobshop.ThankYouActivity">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">

<TextView
android:id="@+id/tv_excellent_buy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="@string/excellent_buy"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_tracking_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_excellent_buy"
android:layout_marginTop="18dp"
android:textColor="@android:color/white" />

<TextView
android:id="@+id/tv_support_tel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_tracking_code"
android:layout_marginTop="4dp"
android:text="@string/label_support_shop"
android:textColor="@android:color/white" />

<View
android:id="@+id/vv_horizontal_line_01"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_support_tel"
android:layout_marginBottom="14dp"
android:layout_marginTop="14dp"
android:background="@android:color/white" />

<ImageView
android:id="@+id/iv_sneaker"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_01"
android:background="@drawable/image_view_sneaker"
android:contentDescription="@string/desc_sneaker_bought"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/tv_model"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_sneaker"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/iv_sneaker"
android:layout_toRightOf="@+id/iv_sneaker"
android:textColor="@android:color/white" />

<ImageView
android:id="@+id/iv_brand"
android:layout_width="wrap_content"
android:layout_height="14dp"
android:layout_alignLeft="@+id/tv_model"
android:layout_alignStart="@+id/tv_model"
android:layout_below="@+id/tv_model"
android:layout_marginTop="2dp"
android:layout_toEndOf="@+id/iv_sneaker"
android:layout_toRightOf="@+id/iv_sneaker"
android:contentDescription="@string/desc_sneaker_bought_brand"
android:tint="@color/colorPrimaryLight" />

<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/vv_horizontal_line_01"
android:textColor="@android:color/white"
android:textStyle="italic" />

<View
android:id="@+id/vv_horizontal_line_02"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_sneaker"
android:layout_marginBottom="14dp"
android:layout_marginTop="14dp"
android:background="@android:color/white" />

<Button
android:id="@+id/bt_keep_buying"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@drawable/button_blue_background"
android:onClick="backToTenisShop"
android:text="@string/keep_buying"
android:textAllCaps="false"
android:textColor="@android:color/white" />
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>

 

Os arquivos drawables presentes no layout anterior já foram todos apresentados em seções anteriores, assim podemos ir direto ao diagrama de content_thank_you.xml:

Diagrama do layout content_thank_you.xml

Então o layout principal da atividade de "obrigado pela compra", o XML /res/layout/activity_thank_you.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="thiengo.com.br.tnismobshop.ThankYouActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_thank_you" />
</android.support.design.widget.CoordinatorLayout>

 

Abaixo o diagrama do layout anterior:

Diagrama do layout activity_thank_you.xml

Por fim o código Kotlin da ThankYouActivity:

class ThankYouActivity : AppCompatActivity() {

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_thank_you )
setSupportActionBar( toolbar )
supportActionBar?.setDisplayHomeAsUpEnabled( true )

val sneaker = intent.getParcelableExtra<Sneaker>( Sneaker.KEY )

/*
* Código de rastreamento de compra.
* */
tv_tracking_code.text = String
.format( "%s %s", getString( R.string.track_code_label ), codeBuyGenerator() )

/*
* Modelo tênis.
* */
tv_model.text = sneaker.model

/*
* Marca.
* */
iv_brand.setImageResource( sneaker.brand.imageResource )
iv_brand.contentDescription = sneaker.brand.name

/*
* Imagem.
* */
iv_sneaker.setImageResource( sneaker.imageResource )
iv_sneaker.contentDescription = String
.format( "%s %s", getString( R.string.sneaker ), sneaker.model )
Util.setImageViewBgColor( this, iv_sneaker )

/*
* Preço.
* */
tv_price.text = sneaker.getPriceAsString()
}

/*
* Método que simula a criação de uma código de compra
* para rastreamento de mercadoria.
* */
fun codeBuyGenerator(): String {
val random = Random()
var code = ""

for( i in 0..18 ){
code += random.nextInt( 10 ).toString()
}
return code
}

fun backToTenisShop( view: View ){
val it = Intent( this, SneakersActivity::class.java )
/*
* Certificando de que não haverá nenhuma outra atividade
* na pilha de atividades quando o button de volta for
* acionado.
* */
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity( it )
}
}

 

Com isso podemos partir para a evolução do projeto, removendo códigos boilerplate de arquivos dinâmicos quando utilizando a biblioteca Data Binding.

Atualizando o projeto: binding via biblioteca Data Binding

A prioridade aqui é diminuir a quantidade de código boilerplate, estático, em arquivos de linguagem dinâmica, em nosso caso, classes Kotlin.

Vamos aplicar a sintaxe Data Binding na ordem de apresentação das atividades:

  • Atividade principal:
    • Incluindo adaptador de itens.
  • Atividade de detalhes:
    • Incluindo caixa de diálogo para pagamento.
  • Atividade de "obrigado pela compra".

Vamos iniciar com a instalação da API.

Atualizando o Gradle

No Gradle App Level, ou build.gradle (Module: app), adicione as configurações em destaque:

...
apply plugin: 'kotlin-kapt'

android {
...
dataBinding {
enabled = true
}
}

dependencies {
...
kapt "com.android.databinding:compiler:$android_plugin_version"
}

 

Sincronize o projeto.

Colocando o bind de dados no cabeçalho do menu gaveta

Antes de partir para o código bind de objeto do tipo User ao cabeçalho do menu gaveta, temos de entender um problema.

A classe User tem como uma de suas propriedades um inteiro de recurso de imagem. O atributo android:src da RoundedImageView aceita somente path válido de acesso a imagem. Sendo assim, temos de criar um método para devolver o path da imagem de acordo com o ID local dela.

Já lhe adianto que este novo método será útil em outros contextos do projeto, logo, na classe utilitária, Util, adicione o código a seguir:

class Util {
companion object {
...
/*
* Para que o path da imagem seja gerado e então vinculado
* ao atributo android:src do ImageView (ou subclasses) de layout.
* */
@JvmStatic
fun getImageSource( context: Context, image: Int ) : String =
"android.resource://" + context.packageName + "/" + image
}
}

 

Com isso podemos partir para a atualização do layout de cabeçalho, /res/layout/nav_header_sneaker.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data>
<import type="thiengo.com.br.tnismobshop.util.Util" />

<import type="thiengo.com.br.tnismobshop.domain.User" />

<variable
name="user"
type="User" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar"
android:gravity="bottom"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark">

<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:scaleType="centerCrop"
android:src="@{ Util.getImageSource( context, user.image ) }"
app:riv_border_color="@color/colorPrimaryDark"
app:riv_border_width="2dp"
app:riv_corner_radius="5dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />

<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:text="@{ user.name }"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

<TextView
android:id="@+id/tv_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ user.email }" />
</LinearLayout>
</layout>

 

A seguir a nova estrutura XML do layout anterior:

Nova estrutura XML do layout nav_header_sneaker.xml

Assim o novo código bind de user ao cabeçalho do menu gaveta, código presente em SneakersActivity:

class SneakersActivity : ... {
...

private fun initHeader(){
val user = Database.getUser()

/*
* Realizando o bind somente do layout de cabeçalho
* do menu gaveta.
* */
val navHeader = NavHeaderSneakerBinding
.bind( nav_view.getHeaderView(0) )
navHeader.user = user
}
}

 

Note que não foi necessário modificar nada no layout container do menu gaveta, somente no layout de cabeçalho.

Bind do layout de itens de lista

Antes de irmos direto a atualização do layout de item de lista, vamos primeiro recordar como uma imagem de tênis é configurada no layout de item em SneakersAdapter:

...
private fun setGallery( sneaker: Sneaker ){
/*
* Imagem principal do tênis.
* */
ivSneaker.setImageResource( sneaker.imageResource )
ivSneaker.contentDescription = String
.format( "%s %s", context.getString( R.string.sneacker_label ), sneaker.model )

ivSneakerGallery01.setImageResource( sneaker.album[0] )
ivSneakerGallery02.setImageResource( sneaker.album[1] )
ivSneakerGallery03.setImageResource( sneaker.album[2] )

/*
* Atualizando a cor de background das imagens para que
* a borda fique sem contraste.
* */
Util.setImageViewBgColor( context, ivSneaker )
Util.setImageViewBgColor( context, ivSneakerGallery01 )
Util.setImageViewBgColor( context, ivSneakerGallery02 )
Util.setImageViewBgColor( context, ivSneakerGallery03 )
}
...

 

O método setImageViewBgColor() é responsável por acessar o shape de background da imagem e então mudar a cor de fundo dele, para que as bordas correspondam à cor de fundo da imagem.

Ou seja, tanto este método quanto o vinculo de imagens via setImageResource() deverá ainda ocorrer e o método utilitário Util.getImageSource() não responde a essas necessidades.

Nossa solução será criar um método @BindingAdapter que responderá a dois atributos personalizados que colocaremos não somente no layout de item de lista, mas também em todos os layouts que têm de apresentar imagens de tênis.

Na classe utilitária, adicione o método a seguir:

class Util {
companion object {
...

/*
* Criando um configurador personalizado para que seja
* possível adicionar a imagem de tênis ao ImageView
* e também invocar o método Util.setImageViewBgColor()
* para a configuração de borda de imagem.
* */
@JvmStatic
@BindingAdapter( "app:context", "app:src" )
fun setConfImage( iv: ImageView, context: Context, image: Int ) {
iv.setImageResource( image )
Util.setImageViewBgColor( context, iv )
}
}
}

 

Com isso podemos atualizar o layout /res/layout/sneaker.xml:

<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context="thiengo.com.br.tnismobshop.SneakersActivity">


<data>
<import type="thiengo.com.br.tnismobshop.util.Util" />

<import type="android.view.View" />

<variable
name="sneaker"
type="thiengo.com.br.tnismobshop.domain.Sneaker" />
</data>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="7dp"
android:layout_marginLeft="14dp"
android:layout_marginRight="14dp"
android:layout_marginTop="7dp"
android:background="@drawable/item_border_and_background"
android:padding="6dp">

<ImageView
android:id="@+id/iv_sneaker"
android:layout_width="81dp"
android:layout_height="81dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:background="@drawable/image_view_sneaker"
android:contentDescription='@{ String.format("%s %s", @string/sneacker_label, sneaker.model) }'
android:scaleType="fitCenter"
app:context="@{ context }"
app:src="@{ sneaker.imageResource }" />

<ImageView
android:id="@+id/iv_sneaker_gallery_01"
style="@style/AppTheme.ItemImgGallery"
android:layout_alignTop="@+id/iv_sneaker"
android:contentDescription="@string/desc_sneaker_first_gallery_image"
app:context="@{ context }"
app:src="@{ sneaker.album[0] }" />

<ImageView
android:id="@+id/iv_sneaker_gallery_02"
style="@style/AppTheme.ItemImgGallery"
android:layout_below="@+id/iv_sneaker_gallery_01"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/desc_sneaker_second_gallery_image"
app:context="@{ context }"
app:src="@{ sneaker.album[1] }" />

<ImageView
android:id="@+id/iv_sneaker_gallery_03"
style="@style/AppTheme.ItemImgGallery"
android:layout_below="@+id/iv_sneaker_gallery_02"
android:contentDescription="@string/desc_sneaker_third_gallery_image"
app:context="@{ context }"
app:src="@{ sneaker.album[2] }" />

<View
android:id="@+id/vv_vertical_line"
android:layout_width="1dp"
android:layout_height="81dp"
android:layout_alignParentTop="true"
android:layout_marginEnd="6dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginStart="6dp"
android:layout_toEndOf="@+id/iv_sneaker_gallery_01"
android:layout_toRightOf="@+id/iv_sneaker_gallery_01"
android:background="@android:color/white" />

<TextView
android:id="@+id/tv_model"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/tv_free_delivery"
android:layout_alignParentTop="true"
android:layout_alignRight="@+id/tv_free_delivery"
android:layout_toEndOf="@id/vv_vertical_line"
android:layout_toRightOf="@id/vv_vertical_line"
android:ellipsize="end"
android:maxLines="1"
android:text="@{ sneaker.model }"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />

<ImageView
android:id="@+id/iv_brand"
android:layout_width="wrap_content"
android:layout_height="14dp"
android:layout_below="@+id/tv_model"
android:layout_marginBottom="4dp"
android:layout_toEndOf="@id/vv_vertical_line"
android:layout_toRightOf="@id/vv_vertical_line"
android:contentDescription="@{ sneaker.brand.name }"
android:scaleType="fitCenter"
android:src="@{ Util.getImageSource( context, sneaker.brand.imageResource ) }"
android:tint="@color/colorPrimaryLight" />

<ImageView
android:id="@+id/iv_genre_male"
style="@style/AppTheme.ItemIconGenre"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:layout_toEndOf="@id/vv_vertical_line"
android:layout_toRightOf="@id/vv_vertical_line"
android:contentDescription="@string/desc_male_icon"
android:src="@drawable/ic_gender_male_white_18dp"
android:visibility="@{ sneaker.isForMale ? View.VISIBLE : View.GONE }" />

<ImageView
android:id="@+id/iv_genre_female"
style="@style/AppTheme.ItemIconGenre"
android:layout_toEndOf="@id/iv_genre_male"
android:layout_toRightOf="@id/iv_genre_male"
android:contentDescription="@string/desc_female_icon"
android:src="@drawable/ic_gender_female_white_18dp"
android:visibility="@{ sneaker.isForFemale ? View.VISIBLE : View.GONE }" />

<ImageView
android:id="@+id/iv_rating_star_01"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_marginLeft="0dp"
android:layout_marginStart="0dp"
android:layout_toEndOf="@id/vv_vertical_line"
android:layout_toRightOf="@id/vv_vertical_line"
android:src="@{ 1 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<ImageView
android:id="@+id/iv_rating_star_02"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/iv_rating_star_01"
android:layout_toRightOf="@id/iv_rating_star_01"
android:src="@{ 2 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<ImageView
android:id="@+id/iv_rating_star_03"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/iv_rating_star_02"
android:layout_toRightOf="@id/iv_rating_star_02"
android:src="@{ 3 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<ImageView
android:id="@+id/iv_rating_star_04"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/iv_rating_star_03"
android:layout_toRightOf="@id/iv_rating_star_03"
android:src="@{ 4 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<ImageView
android:id="@+id/iv_rating_star_05"
style="@style/AppTheme.ItemStar"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/iv_rating_star_04"
android:layout_toRightOf="@id/iv_rating_star_04"
android:src="@{ 5 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<TextView
android:id="@+id/tv_rating_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginLeft="1dp"
android:layout_marginStart="1dp"
android:layout_toEndOf="@id/iv_rating_star_05"
android:layout_toRightOf="@id/iv_rating_star_05"
android:text='@{ String.format("%s", sneaker.rating.amount) }'
android:textColor="@android:color/white"
android:textSize="13sp" />

<TextView
android:id="@+id/tv_free_delivery"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:background="@drawable/text_view_free_delivery_background"
android:gravity="end"
android:text="@string/label_free"
android:textColor="@android:color/white" />

<TextView
android:id="@+id/tv_put_shop_cart"
android:layout_width="wrap_content"
android:layout_height="22dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_free_delivery"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:background="@drawable/text_view_shop_cart_background"
android:gravity="start"
android:text="@string/label_put_on_shop_cart"
android:textColor="@android:color/white"
android:textSize="12sp" />

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_put_shop_cart"
android:background="@drawable/dark_green_background"
android:gravity="end"
android:orientation="vertical"
android:paddingBottom="1dp"
android:paddingLeft="4dp"
android:paddingRight="4dp">

<TextView
android:id="@+id/tv_price_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ sneaker.getPriceParcelsAsString() }"
android:textColor="@android:color/white"
android:textSize="13sp"
android:textStyle="bold|italic" />

<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:text="@{ sneaker.getPriceAsString() }"
android:textColor="@android:color/white"
android:textSize="9sp"
android:textStyle="bold|italic" />
</LinearLayout>
</RelativeLayout>
</layout>

 

Note que devido ao uso do operador ternário (Java) em código bind, não mais precisamos, no adapter, dos métodos Util.setGenre() e Util.setStar(). Com o mesmo comportamento ocorrendo na atividade de detalhes, poderemos remover estes métodos da classe utilitária.

A seguir o diagrama da nova estrutura do layout sneaker.xml:

Diagrama da nova estrutura do layout sneaker.xml

Então o código atualizado de SneakersAdapter:

class SneakersAdapter(
private val context: Context,
private val sneakers: List<Sneaker> ) :
RecyclerView.Adapter<SneakersAdapter.ViewHolder>() {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int ) : SneakersAdapter.ViewHolder {

val layoutInflater = LayoutInflater
.from( context )

/*
* Vinculando o layout a classe binding dele.
* */
val sneakerBinding = SneakerBinding
.inflate( layoutInflater, parent, false )

return ViewHolder( sneakerBinding )
}
...

inner class ViewHolder( val binding: SneakerBinding ) :
RecyclerView.ViewHolder( binding.root ),
View.OnClickListener {

init {
/*
* A propriedade root contém exatamente o
* ViewGroup container do layout de item.
* O uso do método setOnClickListener() foi
* mantido, pois como em outras partes do
* projeto, o vinculo de listener utilizando
* métodos direto dos objetos de visualização
* é mais simples do que quando utilizando
* a sintaxe Data Binding.
* */
binding.root.setOnClickListener( this )
}

fun setData( sneaker: Sneaker ) {
binding.sneaker = sneaker

/*
* O método executePendingBindings() força o
* binding a ser executado imediatamente, em
* vez de atrasá-lo até o próximo quadro.
* */
binding.executePendingBindings()
}
...
}
}

 

Note que o código de listener de clique em item continuou o mesmo, isso, pois segundo meus testes este era ainda o melhor caminho para colocar o listener vinculado ao item.

Qualquer outro código utilizando a sintaxe da Data Binding API acrescentava ainda mais linhas ao projeto.

Nossa SneakersAdapter saiu de 130 linhas de código para 84 (incluindo ainda mais comentários).

Bind na atividade de detalhes

Para a atividade de detalhes será necessário também modificar o layout principal, pois nele poderemos colocar o título da Toolbar.

A seguir o layout activity_sneaker_details.xml atualizado:

<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context="thiengo.com.br.tnismobshop.SneakerDetailsActivity">

<data>
<variable
name="sneaker"
type="thiengo.com.br.tnismobshop.domain.Sneaker" />
</data>

<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="@{ sneaker.model }" />

</android.support.design.widget.AppBarLayout>

<include
layout="@layout/content_sneaker_details"
app:sneaker="@{ sneaker }" />

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:onClick="onClick"
app:srcCompat="@drawable/ic_share_white_24dp" />

</android.support.design.widget.CoordinatorLayout>
</layout>

 

Então o diagrama da nova estrutura do layout activity_sneaker_details.xml:

Diagrama da nova estrutura do layout activity_sneaker_details.xml

Agora o layout de conteúdo, atualizado, /res/layout/content_sneaker_details.xml. Note que o conjunto app:context e app:src volta a ser utilizado aqui devido a presença de imagens de tênis:

<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context="thiengo.com.br.tnismobshop.SneakerDetailsActivity"
tools:showIn="@layout/activity_sneaker_details">

<data>
<import type="thiengo.com.br.tnismobshop.util.Util" />

<import type="android.view.View" />

<variable
name="sneaker"
type="thiengo.com.br.tnismobshop.domain.Sneaker" />
</data>

<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="14dp">

<ImageView
android:id="@+id/iv_brand"
android:layout_width="40dp"
android:layout_height="20dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:contentDescription="@{ sneaker.brand.name }"
android:scaleType="fitCenter"
android:src="@{ Util.getImageSource( context, sneaker.brand.imageResource ) }"
android:tint="@color/colorPrimaryLight" />

<View
android:id="@+id/vv_horizontal_line_01"
android:layout_width="40dp"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_brand"
android:layout_marginBottom="3dp"
android:layout_marginTop="14dp"
android:background="@android:color/white" />

<ImageView
android:id="@+id/iv_genre_male"
style="@style/AppTheme.DetailsIconGenre"
android:layout_below="@+id/vv_horizontal_line_01"
android:contentDescription="@string/desc_male_icon"
android:src="@drawable/ic_gender_male_white_18dp"
android:visibility="@{ sneaker.isForMale ? View.VISIBLE : View.GONE }" />

<ImageView
android:id="@+id/iv_genre_female"
style="@style/AppTheme.DetailsIconGenre"
android:layout_below="@+id/iv_genre_male"
android:contentDescription="@string/desc_female_icon"
android:src="@drawable/ic_gender_female_white_18dp"
android:visibility="@{ sneaker.isForFemale ? View.VISIBLE : View.GONE }" />

<View
android:id="@+id/vv_horizontal_line_02"
android:layout_width="40dp"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_genre_female"
android:layout_marginBottom="14dp"
android:layout_marginTop="3dp"
android:background="@android:color/white" />

<Spinner
android:id="@+id/sp_cor"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_02"
android:background="@drawable/spinner_mini_border_and_background"
android:entries="@array/colors"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:popupBackground="@color/colorPrimary"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme" />

<View
android:id="@+id/vv_horizontal_line_03"
android:layout_width="40dp"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/sp_cor"
android:layout_marginBottom="14dp"
android:layout_marginTop="14dp"
android:background="@android:color/white" />

<Spinner
android:id="@+id/sp_size"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_03"
android:background="@drawable/spinner_mini_border_and_background"
android:entries="@array/sizes"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:popupBackground="@color/colorPrimary"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme" />

<View
android:id="@+id/vv_vertical_line_01"
style="@style/AppTheme.DetailsVerticalLine"
android:layout_toEndOf="@+id/iv_brand"
android:layout_toRightOf="@+id/iv_brand" />

<LinearLayout
android:id="@+id/ll_center_and_right"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/vv_vertical_line_01"
android:layout_toRightOf="@+id/vv_vertical_line_01"
android:orientation="horizontal">

<ImageView
android:id="@+id/iv_sneaker_01"
android:layout_width="0dp"
android:layout_height="240dp"
android:layout_weight="1"
android:background="@drawable/image_view_sneaker"
android:contentDescription='@{ String.format("%s %s", @string/sneaker, sneaker.model) }'
android:scaleType="fitCenter"
app:context="@{ context }"
app:src="@{ sneaker.imageResource }" />

<View
android:id="@+id/vv_vertical_line_02"
style="@style/AppTheme.DetailsVerticalLine" />

<LinearLayout
android:id="@+id/ll_gallery"
android:layout_width="68dp"
android:layout_height="240dp"
android:orientation="vertical">

<ImageView
android:id="@+id/iv_sneaker_02"
style="@style/AppTheme.DetailsImgGallery"
app:context="@{ context }"
app:src="@{ sneaker.album[0] }" />

<ImageView
android:id="@+id/iv_sneaker_03"
style="@style/AppTheme.DetailsImgGallery"
android:layout_marginBottom="14dp"
android:layout_marginTop="14dp"
app:context="@{ context }"
app:src="@{ sneaker.album[1] }" />

<ImageView
android:id="@+id/iv_sneaker_04"
style="@style/AppTheme.DetailsImgGallery"
app:context="@{ context }"
app:src="@{ sneaker.album[2] }" />

</LinearLayout>
</LinearLayout>

<RelativeLayout
android:id="@+id/rl_buy_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/ll_center_and_right"
android:layout_marginTop="14dp"
android:background="@drawable/dark_green_background"
android:padding="6dp">

<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text='@{ String.format("%s %s", sneaker.getPriceAsString(), @string/or_in) }'
android:textColor="@android:color/white"
android:textSize="17sp"
android:textStyle="bold|italic" />

<TextView
android:id="@+id/tv_price_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_price"
android:layout_marginTop="1dp"
android:text='@{ String.format("%s %s", @string/until, sneaker.getPriceParcelsAsString()) }'
android:textColor="@android:color/white"
android:textSize="17sp"
android:textStyle="bold|italic" />

<View
android:id="@+id/vv_vertical_line_03"
style="@style/AppTheme.DetailsSmalVerticalLine"
android:layout_alignTop="@+id/tv_price"
android:layout_toEndOf="@+id/tv_price"
android:layout_toRightOf="@+id/tv_price" />

<TextView
android:id="@+id/tv_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/vv_vertical_line_04"
android:layout_alignLeft="@+id/vv_vertical_line_03"
android:layout_alignParentTop="true"
android:layout_alignRight="@+id/vv_vertical_line_04"
android:layout_alignStart="@+id/vv_vertical_line_03"
android:layout_below="@+id/tv_price"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="1dp"
android:ellipsize="end"
android:maxLines="1"
android:text='@{ sneaker.extraInfo.getStockFormatted( context ) }'
android:textColor="@android:color/white"
android:textSize="9sp" />

<Spinner
android:id="@+id/sp_amount"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_alignEnd="@+id/vv_vertical_line_04"
android:layout_alignLeft="@+id/vv_vertical_line_03"
android:layout_alignRight="@+id/vv_vertical_line_04"
android:layout_alignStart="@+id/vv_vertical_line_03"
android:layout_below="@+id/tv_amount"
android:layout_gravity="center"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:background="@drawable/spinner_mini_border_and_background"
android:entries="@array/amount"
android:gravity="center"
android:paddingBottom="2dp"
android:paddingTop="2dp"
android:popupBackground="@color/colorPrimary"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme" />

<View
android:id="@+id/vv_vertical_line_04"
style="@style/AppTheme.DetailsSmalVerticalLine"
android:layout_alignTop="@+id/tv_price"
android:layout_toLeftOf="@+id/bt_buy"
android:layout_toStartOf="@+id/bt_buy" />

<Button
android:id="@+id/bt_buy"
style="@style/AppTheme.DetailsButton"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:onClick="onClick"
android:text="@string/label_buy" />
</RelativeLayout>

<TextView
android:id="@+id/tv_free_delivery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/rl_buy_data"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:text="@string/brazil_free_delivery"
android:textColor="@color/colorFreeDelivery"
android:textSize="12sp" />

<ImageView
android:layout_width="16dp"
android:layout_height="15dp"
android:layout_alignTop="@+id/tv_free_delivery"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:layout_marginTop="1dp"
android:layout_toLeftOf="@+id/tv_free_delivery"
android:layout_toStartOf="@+id/tv_free_delivery"
android:contentDescription="@string/desc_truck_delivery"
android:src="@drawable/ic_truck_white_24dp" />

<View
android:id="@+id/vv_horizontal_line_04"
style="@style/AppTheme.DetailsHorizontalLine"
android:layout_below="@+id/tv_free_delivery" />

<TextView
android:id="@+id/tv_label_recommended"
style="@style/AppTheme.DetailsContentLabel"
android:layout_below="@+id/vv_horizontal_line_04"
android:text="@string/label_recommended" />

<TextView
android:id="@+id/tv_recommended"
style="@style/AppTheme.DetailsContent"
android:layout_alignTop="@+id/tv_label_recommended"
android:layout_toEndOf="@+id/tv_label_recommended"
android:layout_toRightOf="@+id/tv_label_recommended"
android:text="@{ sneaker.extraInfo.recommended }" />

<TextView
android:id="@+id/tv_label_type"
style="@style/AppTheme.DetailsContentLabel"
android:layout_below="@+id/tv_label_recommended"
android:text="@string/label_type" />

<TextView
android:id="@+id/tv_type"
style="@style/AppTheme.DetailsContent"
android:layout_alignTop="@+id/tv_label_type"
android:layout_toEndOf="@+id/tv_label_type"
android:layout_toRightOf="@+id/tv_label_type"
android:text="@{ sneaker.extraInfo.type }" />

<TextView
android:id="@+id/tv_label_composition"
style="@style/AppTheme.DetailsContentLabel"
android:layout_below="@+id/tv_label_type"
android:text="@string/label_composition" />

<TextView
android:id="@+id/tv_composition"
style="@style/AppTheme.DetailsContent"
android:layout_alignTop="@+id/tv_label_composition"
android:layout_toEndOf="@+id/tv_label_composition"
android:layout_toRightOf="@+id/tv_label_composition"
android:text="@{ sneaker.extraInfo.composition }" />

<TextView
android:id="@+id/tv_label_desc"
style="@style/AppTheme.DetailsContentLabel"
android:layout_below="@+id/tv_label_composition"
android:text="@string/label_description" />

<TextView
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/tv_label_desc"
android:layout_toEndOf="@+id/tv_label_desc"
android:layout_toRightOf="@+id/tv_label_desc"
android:text="@{ sneaker.extraInfo.fullDescription }"
android:textColor="@android:color/white" />

<View
android:id="@+id/vv_horizontal_line_05"
style="@style/AppTheme.DetailsHorizontalLine"
android:layout_below="@+id/tv_desc"
android:layout_marginBottom="14dp"
android:layout_marginTop="9dp" />

<LinearLayout
android:id="@+id/rl_rating"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/vv_horizontal_line_05"
android:background="@drawable/dark_green_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="6dp">

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingEnd="20dp"
android:paddingLeft="14dp"
android:paddingRight="20dp"
android:paddingStart="14dp">

<ImageView
android:id="@+id/iv_rating_star_01"
style="@style/AppTheme.DetailsStar"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="0dp"
android:layout_marginStart="0dp"
android:src="@{ 1 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<ImageView
android:id="@+id/iv_rating_star_02"
style="@style/AppTheme.DetailsStar"
android:layout_toEndOf="@id/iv_rating_star_01"
android:layout_toRightOf="@id/iv_rating_star_01"
android:src="@{ 2 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<ImageView
android:id="@+id/iv_rating_star_03"
style="@style/AppTheme.DetailsStar"
android:layout_toEndOf="@id/iv_rating_star_02"
android:layout_toRightOf="@id/iv_rating_star_02"
android:src="@{ 3 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<ImageView
android:id="@+id/iv_rating_star_04"
style="@style/AppTheme.DetailsStar"
android:layout_toEndOf="@id/iv_rating_star_03"
android:layout_toRightOf="@id/iv_rating_star_03"
android:src="@{ 4 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<ImageView
android:id="@+id/iv_rating_star_05"
style="@style/AppTheme.DetailsStar"
android:layout_toEndOf="@id/iv_rating_star_04"
android:layout_toRightOf="@id/iv_rating_star_04"
android:src="@{ 5 &lt;= sneaker.rating.stars ? @drawable/ic_star_black_18dp : @drawable/ic_star_border_white_18dp }" />

<TextView
android:id="@+id/tv_rating_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginLeft="2dp"
android:layout_marginStart="2dp"
android:layout_marginTop="-2dp"
android:layout_toEndOf="@id/iv_rating_star_05"
android:layout_toRightOf="@id/iv_rating_star_05"
android:text='@{ String.format("%d", sneaker.rating.amount) }'
android:textColor="@android:color/white" />
</RelativeLayout>

<Button
style="@style/AppTheme.DetailsButton"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/label_access_rating" />
</LinearLayout>
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>
</layout>

 

A seguir o diagrama atualizado da estrutura XML do layout content_sneaker_details.xml:

Diagrama atualizado da estrutura do layout content_sneaker_details.xml

Então o código Kotlin atualizado de SneakerDetailsActivity:

class SneakerDetailsActivity : ... {
...

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
/*
* Realizando o bind exatamente onde havia o setContentView(),
* assim poderemos utilizar as Views em código como elas
* eram utilizadas com o setContentView(), isso, pois o vinculo
* de layout também ocorre na invocação de
* DataBindingUtil.setContentView().
* */
val activityBinding = DataBindingUtil
.setContentView<ActivitySneakerDetailsBinding>(
this,
R.layout.activity_sneaker_details
)

setSupportActionBar( toolbar )
supportActionBar?.setDisplayHomeAsUpEnabled( true )

sneaker = intent.getParcelableExtra( Sneaker.KEY )
activityBinding.sneaker = sneaker
sp_amount.setOnItemSelectedListener( this )
}
...
}

 

Como realizado no código de SneakersAdapter, para o listener de clique. Aqui mantivemos o vinculo de listener ao Spinner sp_amount, pois depois de alguns testes foi constatado este era o melhor caminho em termos de leitura e quantidade de linhas de código.

Os listener de clique para o FloatingActionButton e para o botão de compra do layout somente passaram a utilizar o atributo android:onClick, ou seja, binding de método no modelo tradicional, pois assim, ao menos neste projeto, o ganho é maior do que quando utilizando o binding via Data Binding.

Bind na caixa de diálogo para pagamento

Vamos direto a atualização do layout da caixa de diálogo, o /res/layout/dialog_payment.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<import type="java.util.Locale" />

<variable
name="sneaker"
type="thiengo.com.br.tnismobshop.domain.Sneaker" />

<variable
name="amountSneakers"
type="Integer" />
</data>

<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fillViewport="true">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp">

<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/dialog_title"
android:textColor="@color/colorDarkDialog"
android:textSize="25sp" />

<View
android:id="@+id/vv_horizontal_line_01"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_title"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:background="@color/colorDialogLine" />

<ImageView
android:id="@+id/iv_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_01"
android:contentDescription="@string/desc_credit_cards"
android:scaleType="fitCenter"
android:src="@drawable/credit_cards" />

<View
android:id="@+id/vv_horizontal_line_02"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_cards"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:background="@color/colorDialogLine" />

<TextView
android:id="@+id/tv_card_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_02"
android:text="@string/dialog_label_card_number"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<EditText
android:id="@+id/et_card_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_card_number"
android:layout_toLeftOf="@+id/et_card_cvv"
android:layout_toStartOf="@+id/et_card_cvv"
android:background="@drawable/field_border_and_background_dark"
android:hint="@string/dialog_hint_card_number"
android:inputType="number"
android:padding="10dp"
android:textColor="@color/colorDarkDialog" />

<TextView
android:id="@+id/tv_card_cvv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/et_card_cvv"
android:layout_alignStart="@+id/et_card_cvv"
android:layout_below="@+id/vv_horizontal_line_02"
android:text="@string/dialog_label_card_cvv"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<EditText
android:id="@+id/et_card_cvv"
android:layout_width="140dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_card_cvv"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:background="@drawable/field_border_and_background_dark"
android:hint="@string/dialog_hint_card_cvv"
android:inputType="number"
android:padding="10dp"
android:textColor="@color/colorDarkDialog" />

<TextView
android:id="@+id/tv_card_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/et_card_number"
android:layout_marginTop="12dp"
android:text="@string/dialog_label_card_owner"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<EditText
android:id="@+id/et_card_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_card_name"
android:background="@drawable/field_border_and_background_dark"
android:inputType="textPersonName"
android:padding="10dp"
android:textColor="@color/colorDarkDialog" />

<TextView
android:id="@+id/tv_card_expired"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/et_card_name"
android:layout_marginTop="12dp"
android:text="@string/dialog_label_card_expire"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_card_expired_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_card_expired"
android:layout_marginTop="-2dp"
android:text="@string/dialog_label_card_expire_month"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<Spinner
android:id="@+id/sp_card_expired_month"
android:layout_width="44dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_card_expired_month"
android:background="@drawable/spinner_mini_border_and_background_dark"
android:entries="@array/months"
android:paddingBottom="12dp"
android:paddingTop="12dp"
android:popupBackground="@color/colorDialogPopUpSpinner"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerThemeDark" />

<TextView
android:id="@+id/tv_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/sp_card_expired_month"
android:layout_marginEnd="6dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginStart="6dp"
android:layout_marginTop="7dp"
android:layout_toEndOf="@+id/sp_card_expired_month"
android:layout_toRightOf="@+id/sp_card_expired_month"
android:text="@string/dialog_label_card_expire_bar"
android:textColor="@color/colorDarkDialog"
android:textSize="20sp" />

<TextView
android:id="@+id/tv_card_expired_year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/sp_card_expired_year"
android:layout_alignStart="@+id/sp_card_expired_year"
android:layout_alignTop="@+id/tv_card_expired_month"
android:text="@string/dialog_label_card_expire_year"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<Spinner
android:id="@+id/sp_card_expired_year"
android:layout_width="61dp"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_card_expired_year"
android:layout_toEndOf="@+id/tv_bar"
android:layout_toRightOf="@+id/tv_bar"
android:background="@drawable/spinner_mini_border_and_background_dark"
android:entries="@array/years"
android:paddingBottom="12dp"
android:paddingTop="12dp"
android:popupBackground="@color/colorDialogPopUpSpinner"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerThemeDark" />

<View
android:id="@+id/vv_vertical_line_01"
android:layout_width="1dp"
android:layout_height="68dp"
android:layout_alignTop="@+id/tv_card_expired"
android:layout_marginBottom="6dp"
android:layout_marginEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:layout_toEndOf="@+id/sp_card_expired_year"
android:layout_toRightOf="@+id/sp_card_expired_year"
android:background="@color/colorDialogLine" />

<TextView
android:id="@+id/tv_buy_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/tv_card_expired"
android:layout_toEndOf="@+id/vv_vertical_line_01"
android:layout_toRightOf="@+id/vv_vertical_line_01"
android:text="@string/dialog_label_card_total_parcels"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<Spinner
android:id="@+id/sp_buy_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_buy_parcels"
android:layout_toEndOf="@+id/vv_vertical_line_01"
android:layout_toRightOf="@+id/vv_vertical_line_01"
android:background="@drawable/spinner_mini_border_and_background_dark"
android:entries="@array/parcels"
android:paddingBottom="12dp"
android:paddingTop="12dp"
android:popupBackground="@color/colorDialogPopUpSpinner"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerThemeDark" />

<Button
android:id="@+id/bt_finish_buy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/vv_vertical_line_01"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignStart="@+id/vv_vertical_line_01"
android:layout_below="@+id/sp_buy_parcels"
android:layout_marginTop="12dp"
android:background="@drawable/button_blue_background"
android:onClick="onClick"
android:text="@string/label_bt_finish_buy"
android:textAllCaps="false"
android:textColor="@android:color/white" />

<TextView
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignTop="@+id/bt_finish_buy"
android:text="@string/dialog_label_total"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_total_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_total"
android:layout_marginTop="-4dp"
android:text='@{ String.format(Locale.FRANCE, "R$ %.2f", sneaker.price * amountSneakers) }'
android:textColor="@color/colorDarkDialog"
android:textSize="21sp"
android:textStyle="bold|italic" />

<TextView
android:id="@+id/tv_total_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_total_price"
android:layout_marginTop="-4dp"
android:text="@string/dialog_label_parcels_without_taxes"
android:textColor="@color/colorDarkDialog"
android:textSize="11sp"
android:textStyle="bold" />
</RelativeLayout>
</ScrollView>
</layout>

 

Então o diagrama da nova estrutura XML do layout dialog_payment.xml:

Diagrama da nova estrutura do layout dialog_payment.xml

Por fim o código do método callPaymentDialog(), em SneakerDetailsActivity, atualizado:

...
fun callPaymentDialog() {
/*
* Inflando o layout junto a classe binding dele.
* */
val binding = DialogPaymentBinding
.inflate( layoutInflater)

binding.sneaker = sneaker
binding.amountSneakers = amount

val builder = AlertDialog.Builder( this )

/*
* Colocando o layout bound como custom layout do
* AlertDialog.
* */
builder.setView( binding.root )

builder.create().show()
}
...

Bind na atividade de "Obrigado pela compra"

Na ThankYouActivity temos o método codeBuyGenerator() que é invocado em um String.format(), este último que certamente colocaremos em layout via bind.

Porém não temos acesso ao objeto da atividade host do layout, logo, uma possível solução é mover codeBuyGenerator() para outro ponto do projeto.

Escolhi coloca-lo na classe utilitária, segue atualização:

class Util {
companion object {
...
/*
* Método que simula a criação de um código de compra
* para rastreamento de mercadoria.
* */
@JvmStatic
fun codeBuyGenerator(): String {
val random = Random()
var code = ""

for( i in 0..18 ){
code += random.nextInt( 10 ).toString()
}
return code
}
}
}

 

Assim podemos atualizar o código do layout content_thank_you.xml, pois o layout principal da ThankYouActivity permanecerá o mesmo:

<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context="thiengo.com.br.tnismobshop.ThankYouActivity">


<data>
<import type="thiengo.com.br.tnismobshop.util.Util" />

<import type="java.util.Locale" />

<variable
name="sneaker"
type="thiengo.com.br.tnismobshop.domain.Sneaker" />

<variable
name="amountSneakers"
type="Integer" />
</data>

<android.support.v4.widget.NestedScrollView
android:id="@+id/nsv_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">

<TextView
android:id="@+id/tv_excellent_buy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="@string/excellent_buy"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_tracking_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_excellent_buy"
android:layout_marginTop="18dp"
android:text='@{ String.format("%s %s", @string/track_code_label, Util.codeBuyGenerator()) }'
android:textColor="@android:color/white" />

<TextView
android:id="@+id/tv_support_tel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_tracking_code"
android:layout_marginTop="4dp"
android:text="@string/label_support_shop"
android:textColor="@android:color/white" />

<View
android:id="@+id/vv_horizontal_line_01"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_support_tel"
android:layout_marginBottom="14dp"
android:layout_marginTop="14dp"
android:background="@android:color/white" />

<ImageView
android:id="@+id/iv_sneaker"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/vv_horizontal_line_01"
android:background="@drawable/image_view_sneaker"
android:contentDescription='@{ String.format( "%s %s", @string/sneaker, sneaker.model ) }'
android:scaleType="fitCenter"
app:context="@{ context }"
app:src="@{ sneaker.imageResource }" />

<TextView
android:id="@+id/tv_model"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_sneaker"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/iv_sneaker"
android:layout_toRightOf="@+id/iv_sneaker"
android:text="@{ sneaker.model }"
android:textColor="@android:color/white" />

<ImageView
android:id="@+id/iv_brand"
android:layout_width="wrap_content"
android:layout_height="14dp"
android:layout_alignLeft="@+id/tv_model"
android:layout_alignStart="@+id/tv_model"
android:layout_below="@+id/tv_model"
android:layout_marginTop="2dp"
android:layout_toEndOf="@+id/iv_sneaker"
android:layout_toRightOf="@+id/iv_sneaker"
android:contentDescription="@{ sneaker.brand.name }"
android:src="@{ Util.getImageSource( context, sneaker.brand.imageResource ) }"
android:tint="@color/colorPrimaryLight" />

<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/vv_horizontal_line_01"
android:text='@{ String.format(Locale.FRANCE, "R$ %.2f", sneaker.price * amountSneakers) }'
android:textColor="@android:color/white"
android:textStyle="italic" />

<View
android:id="@+id/vv_horizontal_line_02"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_sneaker"
android:layout_marginBottom="14dp"
android:layout_marginTop="14dp"
android:background="@android:color/white" />

<Button
android:id="@+id/bt_keep_buying"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@drawable/button_blue_background"
android:onClick="backToTenisShop"
android:text="@string/keep_buying"
android:textAllCaps="false"
android:textColor="@android:color/white" />
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>
</layout>

 

A seguir o diagrama da nova estrutura XML do layout content_thank_you.xml:

Diagrama da nova estrutura do layout content_thank_you.xml

E então o código atualizado de ThankYouActivity:

class ThankYouActivity : AppCompatActivity() {

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_thank_you )
setSupportActionBar( toolbar )
supportActionBar?.setDisplayHomeAsUpEnabled( true )

/*
* Realizando o binding de Sneaker com o layout de conteúdo
* (content_thank_you) de ThankYouActivity.
* */
val binding = ContentThankYouBinding.bind( nsv_container )

binding.sneaker = intent.getParcelableExtra( Sneaker.KEY )
binding.amountSneakers = intent.getIntExtra( Sneaker.KEY_AMOUNT, 0 )
}
...
}

Testes e resultados

Acesse o menu de topo do Android Studio. Acione Build e em seguida Rebuild project. Agora execute o aplicativo em seu emulador ou aparelho de testes.

Iniciando o app Android e navegando pelas telas, temos que o comportamento continua o mesmo, porém o código está com menos trechos boilerplate:

Animação de app Android com API Data Binding

Assim terminamos o estudo da biblioteca Data Binding.

Não deixe de se inscrever na 📩 lista de emails do Blog, logo acima ou ao lado, para receber em primeira mão os conteúdos exclusivos sobre o desenvolvimento Android.

Se inscreva também no canal do Blog em: YouTube Thiengo.

Slides

Abaixo os slides com a apresentação da biblioteca Data Binding:

Vídeos

A seguir os vídeos com a atualização passo a passo do aplicativo Android para vendas de tênis:

Para acessar o projeto de exemplo, entre no GitHub dele em: https://github.com/viniciusthiengo/tenis-mob-shop.

Conclusão

A biblioteca Data Binding é parte do conteúdo Android que hoje um desenvolvedor profissional tem que conhecer, isso devido aos benefícios conseguidos com ela, liberando ainda mais espaço e tempo para os códigos de domínio de problema.

É comum empresas que estão em busca de desenvolvedores Android solicitarem também o conhecimento da Data Binding API.

Logo, após o estudo do artigo acima, não deixe de também visitar a documentação oficial para conhecer os trechos avançados de trabalho com a biblioteca.

Assim finalizamos o artigo. Caso você tenha alguma dica ou dúvida sobre binding de dados no Android, deixe logo abaixo nos comentários.

E se curtiu o conteúdo, não esqueça de compartilha-lo. E por fim, não deixe de se inscrever na 📩 lista de emails.

Abraço.

Fontes

Documentação oficial Data Binding Android

Annotation Processing with Kotlin

Loading images with data binding and Picasso

Android Data Binding: 2-way Your Way

Binding Spinner in Android using DataBinding & InverseDataBinding

Android Data Binding: RecyclerView

How to use Data Binding and Kotlin in Android Studio 3.0.0 - Resposta de Rubber Duck

How to data bind to a header? - Resposta de Ready Android

Android databinding using && logical operator - Resposta de Leonardo Acevedo

How to use data-binding in Dialog? - Resposta de IHeartAndroid

How to use data-binding with Fragment - Resposta de hdioui abdeljalil e de Paweł Szczur

Android recyclerview adapter with multiple viewtype using databinding - Resposta de macros013

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
Utilizando Intenções Para Mapas de Alta Qualidade no AndroidUtilizando Intenções Para Mapas de Alta Qualidade no AndroidAndroid
Lottie API Para Animações no AndroidLottie API Para Animações no AndroidAndroid
PhotoView Android Para a Completa Implementação de ZoomPhotoView Android Para a Completa Implementação de ZoomAndroid

Compartilhar

Comentários Facebook

Comentários Blog (6)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
Arthur Santos (1) (0)
09/10/2018
O findViewById dentro do view holder não é necessário, você pode tranquilamente usar o itemView.id_da_minha_view, ou binding.root.id_da_minha_view, vai funcionar perfeitamente
Responder
Vinícius Thiengo (1) (0)
09/10/2018
Arthur,

Você está certo. Obrigado pela contribuição.

Abraço.
Responder
Gabriel Kioshi (1) (0)
14/09/2018
Excelente artigo Thiengo, como sempre conteúdo de qualidade e muito bem explicado. Com certeza irei consultar e recomendar várias vezes. Agora uma pergunta não diretamente sobre o artigo, como você cria os diagramas das estruturas XML para exibir aqui no blog ? Adoraria criar algo semelhante na apresentação de layouts para as minhas apresentações no trabalho e faculdade.
Grande abraço
Responder
Vinícius Thiengo (1) (0)
15/09/2018
Gabriel, tudo bem?

Show de bola que curtiu o conteúdo.

Sobre os diagramas, construo-os no Google Drive, mais precisamente com a ferramenta "Desenhos Google".

Com essa ferramenta é bem simples colocar as formas retangulares bem posicionadas e com as cores necessárias.

Para a construção dos fluxogramas eu utilizo o https://www.draw.io/ que também é gratuito.

Quando você construir os seus fluxogramas no Draw IO não esqueça de também salvar o XML do fluxograma, assim ele poderá ser aproveitado em novos projetos.

Abraço.
Responder
Samuel Ribeiro (1) (0)
14/09/2018
Como sempre, EXCELENTE!!! Parabéns cara!!
Responder
Vinícius Thiengo (1) (0)
14/09/2018
Samuel, tudo bem?

Show de bola que você curtiu o artigo. Os slides do conteúdo eu consegui liberar somente agora:

https://www.slideshare.net/VinciusThiengo/data-binding-para-vinculo-de-dados-na-ui-android

Abraço.
Responder