Como Utilizar Métodos Binding Adapter no Android

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Como Utilizar Métodos Binding Adapter no Android

Como Utilizar Métodos Binding Adapter no Android

Vinícius Thiengo
(611) (169)
Go-ahead
"Concentre todos seus pensamentos no trabalho que tem em mãos. Os raios solares não queimam até que sejam colocados em foco."
Alexander Graham Bell
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas186
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas936
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas599
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Tudo bem?

Neste artigo vamos passo a passo ao estudo de mais uma importante parte da biblioteca Data Binding, desta vez o foco será nos métodos binding adapter.

Como projeto de exemplo teremos um aplicativo de informações dos estados brasileiros, onde o listener de mudança de conteúdo será configurado via anotação @BindingMethods:

Animação do uso do app Android Brasil - PIB Estados

Se você é novo no assunto Data Binding é importante que estude antes o artigo Data Binding Para Vinculo de Dados na UI Android, pois lá tem a base de entendimento deste novo conteúdo.

Antes de prosseguir, aqui utilizaremos o termo "API" como sinônimo de "biblioteca" (library).

Não deixe de se inscrever 📩 na lista de emails do Blog para ter acesso aos conteúdos exclusivos.

A seguir os tópicos que estaremos estudando:

Métodos binding adapter, funcionamento

Primeiro é importante saber que todas as anotações de prefixo @Binding são anotações "binding adapter" e não somente a anotação @BindingAdapter em específico.

Há três tipos de funcionamento com métodos binding adapter sendo utilizados, são eles:

  • Busca automática por um método setter, método que aceita como argumento o tipo de dado informado no atributo alvo;
  • Definição explicita do método que será utilizado - @BindingMethods;
  • Definição de lógica sobre qual método será utilizado - @BindingAdapter.

O framework de classes de visualização do Android faz internamente uso de anotações @Binding, segundo a documentação. Sendo assim o comportamento padrão é a "busca automática por métodos setters".

Definições customizadas de métodos binding adapter (o que estudaremos neste artigo) sobrescrevem as definições padrões no framework de classes Android.

Busca automática por um método setter

Na busca automática, caso um atributo, oficial Android ou não, seja utilizado, um método set com o parâmetro correto é buscado. Exemplo:

  • Atributo android:text é utilizado tendo como valor @{ car.model };
  • A Data Binding API busca por setText( String ), tendo em mente que a propriedade model é do tipo String;
  • Encontrando o método setText( String ), invoca ele para definir o valor em android:text.

Os passos anteriores para acionamento do correto método setter são os mesmo até para atributos não oficiais, como, por exemplo, app:textCar. Neste caso a Data Binding API buscaria por setTextCar(). Se o argumento informado fosse um do tipo Int, então a API buscaria por um método setTextCar( Int ).

A API tenta realizar a conversão caso necessário, aplicando um cast. O atributo android:text (que aciona o método setText()) é o mais simples como exemplo de conversão, pois o tipo aguardado é um CharSequence, porém frequentemente o tipo fornecido é uma String.

Acesso a método específico, @BindingMethods

A anotação @BindingMethods nos permite direcionar o método que será invocado de acordo com a visualização e o atributo sendo utilizados.

Veja o código a seguir:

@BindingMethods(
value = [
BindingMethod(
type = TitleTextView::class,
attribute = "android:text",
method = "setTextTitle"
),
BindingMethod(
type = TextView::class,
attribute = "android:paddingLeft",
method = "setPaddingLeft"
)
]
)
class TitleTextView: TextView {

constructor( context: Context ) : super( context ) {}

constructor( context: Context, attrs: AttributeSet ) : super( context, attrs ) {}

fun setTextTitle( text: String ){
this.setText( String.format( "Título: %s", text ) )
}

fun setPaddingLeft( padding: Int ){
this.setPadding(
padding,
this.paddingTop,
this.paddingRight,
this.paddingBottom
)
}
}

 

Note que na segunda definição de BindingMethod foi utilizada a superclasse de TitleTextView, mais precisamente a definição TextView::class. Não há problemas quanto a isso, pois o que é válido para a superclasse também é válido para as subclasses.

@BindingMethods aceita quantos BindingMethod forem necessários. Os argumentos de BindingMethod são, em ordem:

  • O tipo da View que terá a alteração do método setter do atributo especificado - type;
  • O atributo que terá um novo método setter vinculado a ele - attribute;
  • O rótulo do novo método setter vinculado ao atributo - method.

A documentação oficial informa que a definição de @BindingMethods pode ser realizada em qualquer classe, incluindo uma classe vazia. Mas segundo alguns testes realizados a classe tem de ser a classe View que será utilizada, caso contrário, ao menos para métodos ou atributos novos (não definidos no framework Android), não haverá vinculo dos métodos binding adapter.

Para melhor entendimento, a definição anterior da classe TitleTextView somente terá efeito se está classe View for utilizada, como a seguir:

...
<thiengo.com.br.bindadaptermethodtest.TitleTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="@{ 15 }"
android:text='@{ "Hello Android World!" }' />
...

 

Caso contrário, utilizando diretamente o widget TextView, por exemplo, nem mesmo o método setPaddingLeft() é invocado. O TextView a seguir não aciona nenhum dos métodos vinculados anteriormente via @BindingMethods:

...
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="@{ 30 }"
android:text='@{ "Hello Android World!" }' />
...

 

Fique ciente que para métodos binding serem invocados, também é preciso o uso da sintaxe binding, ou seja, o valor do atributo entre @{}.

Um outro ponto importante: na documentação o namespace é informado como parte irrelevante, mas quando testando temos que a definição attribute = "text" não aciona o método setTextTitle() como a definição com o namespace attribute = "android:text".

Ou seja, sim, para qualquer método binding o namespace do atributo é importante.

Thiengo, quantos conflitos com a documentação oficial em relação a prática. É isso mesmo?

Sim, provavelmente a documentação oficial não está atualizada de acordo com a evolução da biblioteca Data Binding.

Lógica customizada para acesso a método setter, @BindingAdapter

Se você estudou o primeiro artigo do Blog sobre Data Binding então já deve conhecer a anotação @BindingAdapter, que se não utilizada com as devidas precauções pode ser tão prejudicial quanto os conhecidos "variável global" e "go to".

Diferente de @BindingMethods, métodos @BindingAdapter têm de estar em um contexto de entidade estática, que no Kotlin é a definição do método dentro de uma classe object ou companion object e com a anotação @JvmStatic.

O método a seguir é específico para a atualização do padding de topo de uma View qualquer, e não somente a TitleTextView, que faz uso do atributo android:paddingTop:

class TitleTextView: TextView {
...

object BindingAdapterTests {
@JvmStatic
@BindingAdapter( "android:paddingTop" )
fun setPaddingTop( view: View, padding: Int ){

view.setPadding(
view.paddingLeft,
padding,
view.paddingRight,
view.paddingBottom
)
}
}
}

 

O namespace do atributo também é importante aqui. Não há necessidade de ser atributo já definido no Android, pode ser um qualquer de sua autoria, incluindo o namespace.

O método setPaddingTop() anterior seria seguramente acionado pela definição a seguir:

...
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@{ 20 }"
android:text="Hello World!" />
...

 

O primeiro argumento de um método @BindingAdapter é a View alvo, os argumentos posteriores são os valores dos atributos definidos como argumentos em @BindingAdapter( ... ).

Agora uma versão @BindingAdapter com mais de um atributo sendo necessário (ou não) para acionar um método:

...
object BindingAdapterTests {
...

@JvmStatic
@BindingAdapter(
value = [
"android:paddingBottom",
"android:paddingRight"
],
requireAll = false
)
fun setPaddingBottomRight(
view: View,
paddingBottom: Int,
paddingRight: Int
){

view.setPadding(
view.paddingLeft,
view.paddingTop,
paddingRight,
paddingBottom
)
}
}
...

 

Como requireAll foi definido com o valor false, o uso de qualquer um dos atributos, android:paddingBottom ou android:paddingRight, acionará o método setPaddingBottomRight().

Como Int é um "tipo primitivo", o 0 será o valor do parâmetro que não teve um dado informado em XML.

Em caso de tipo de dado que não é primitivo, coloque a definição de aceitação de null na declaração do parâmetro (exemplo: Tipo?) quando o requireAll for false, caso contrário uma exceção poderá ser gerada.

Acessando o antigo e o novo valor em uma mesma definição de método setter

Caso seja necessário acessar também o valor antigo definido no atributo, a sintaxe para atendimento a essa necessidade é como a seguir:

...
object BindingAdapterTests {
...

@JvmStatic
@BindingAdapter("android:layout_marginLeft")
fun setMarginLeft( view: View, marginOld: Int, marginNew: Int ){

if( marginOld != marginNew ){
val layoutParams = view.layoutParams as LinearLayout.LayoutParams

layoutParams.leftMargin = marginNew
view.requestLayout()
}
}
}
...

 

O método setMarginLeft() será acionado quando houver, por exemplo, definições em layout como abaixo:

...
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@{ 50 }"
android:text="Hello World!" />
...

 

Caso mais de um atributo seja definido, então os primeiros parâmetros depois do parâmetro da visualização alvo é que serão os valores antigos, veja o exemplo:

...
object BindingAdapterTests {
...

@JvmStatic
@BindingAdapter(
value = [
"android:layout_marginTop",
"android:layout_marginBottom"
],
requireAll = true
)
fun setMarginTopBottom(
view: View,
marginTopOld: Int,
marginBottomOld: Int,
marginTopNew: Int,
marginBottomNew: Int
){
...
}
}
...

Conversão de valor via @BindingConversion

Em alguns casos a conversão automática da API Data Binding não funciona, isso, pois a simples sintaxe de cast geraria uma exceção.

Nessas situações nós podemos utilizar a anotação @BindingConversion em um método de contexto estático, como quando utilizando a anotação @BindingAdapter.

O método conversion definido pode ter qualquer rótulo, mas para ele ser invocado é necessário:

  • o tipo de parâmetro correto;
  • e também o tipo de retorno correto.

No exemplo a seguir o atributo android:background está recebendo como valor um tipo Int, pois @color/nome_cor retorna o ID identificador da cor em arquivo XML de cores, porém o tipo de valor aguardado em android:background é um Drawable:

...
<Spinner
android:entries="@array/data"
android:background="@{ user.isProfessor ? @color/colorProfessor : @color/colorNormal }"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
...

 

No código dinâmico podemos ter a seguinte configuração de método de conversão:

...
object BindingConverters{
@JvmStatic
@BindingConversion
fun customConvertColorToDrawable( color: Int ) = ColorDrawable( color )
}
...

 

Como informado anteriormente, o vinculo entre método setter e atributo ocorre pelo uso da anotação @BindingConversion em um método de contexto estático e com os corretos tipos de parâmetro e retorno de função.

Note que de todas as anotações @Binding discutidas neste artigo, somente a @BindingConversion trabalha com métodos que têm valor de retorno, as outras anotações não utilizam valor de retorno.

Pontos negativos

  • A uso de anotação para método de conversão, @BindingConversion, deveria aceitar como argumento o atributo vinculado ao novo método de conversão, algo similar ao que é necessário quando utilizando @BindingAdapter. A falta desse vinculo explicito pode confundir alguns desenvolvedores que estão iniciando na biblioteca Data Binding;
  • A teoria na documentação é bem inconsistente quando comparada à prática da API Data Binding, muitas coisas informadas na documentação divergem da prática, ou seja, provavelmente a documentação, ao menos para a API utilizada no contexto Kotlin, está depreciada;
  • Todos os métodos binding adapters customizados podem oferecer problemas de leitura de código, como acontece com o uso de "variáveis globais" e de sintaxes "go to", caso não muito bem documentados.

Ponto positivo

  • Métodos @BindingMethods podem melhorar a arquitetura do projeto, leitura de código, para novas visualizações que tendem a utilizar os mesmos atributos de widgets nativos Android, porém com um processamento diferente, como fizemos com o TitleTextView.

Considerações finais

Apesar dos inúmeros problemas entre teoria na documentação e a prática da parte de métodos binding da API Data Binding, o conhecimento de uso de @BindingMethods@BindingAdapter e @BindingConversion pode lhe ajudar em algumas situações específicas, como foi o caso da visualização personalizada TitleTextView.

Mas confesso que não vejo o uso dessas anotações sendo algo comum, diferente do restante dos recursos da biblioteca Data Binding.

De qualquer forma, conhecer por completo a Data Binding API é sim importante, primeiro porque o mercado de desenvolvedor Android exigi isso, segundo porque são os problemas específicos de domínio que vão dizer se você está ou não preparado para utilizar os recursos menos convencionais de qualquer API.

Projeto Android

O projeto de exemplo será um aplicativo informativo sobre os estados do Brasil. Algumas informações importantes serão apresentadas como conteúdo, informações como: população e PIB (está última é a informação principal).

Parte do algoritmo do aplicativo é o uso de um listener de mudança de valor em Spinner, listener que adicionaremos utilizando a anotação @BindingMethods.

O projeto será dividido em duas partes:

  • Na primeira parte vamos ao desenvolvimento do aplicativo sem o listener de item selecionado em Spinner;
  • Na segunda parte vamos a adição do listener único de item selecionado.

O projeto de exemplo está presente no seguinte GitHub: https://github.com/viniciusthiengo/brasil-pib-estados-kotlin-android.

Acompanhe o projeto até o final para você também aprender como vincular listeners via métodos binding adapter.

Protótipo estático

A seguir as telas do protótipo estático do projeto:

Tela de abertura

Tela de abertura

Tela principal de informações

Tela principal de informações

Tela principal de informações - Menu aberto

Tela principal de informações - Menu aberto

 


Iniciando o projeto

Em seu Android Studio inicie um novo projeto Kotlin:

  • Nome da aplicação: Brasil - PIB Estados;
  • API mínima: 16 (Android Jelly Bean);
  • Atividade inicial: Empty Activity;
  • Nome da atividade inicial: PIBActivity;
  • Para todos os outros campos, mantenha os valores já definidos por padrão.

Ao final da primeira parte do projeto teremos a seguinte arquitetura:

Arquitetura projeto Android Studio Brasil - PIB Estados

Configurações Gradle

A seguir as configurações do Gradle Level de Projeto, ou build.gradle (Project: BrasilPIBEstados):

buildscript {
ext.kotlin_version = '1.3.0'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()

/*
* Para acesso a Plain-Pie API.
* */
maven { url "https://jitpack.io" }
}
}

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

 

Então a configuração do Gradle Level de Aplicativo, ou build.gradle (Module: app):

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

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

android {
compileSdkVersion 28
defaultConfig {
applicationId "thiengo.com.br.brasil_pibestados"
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'
}
}

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

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'

/*
* Plain-Pie API.
* */
implementation 'com.github.zurche:plain-pie:v0.2.9'

/*
* Para a animação de componentes visuais via
* AndroidViewAnimations API.
* */
implementation 'com.daimajia.easing:library:2.0@aar'
implementation 'com.daimajia.androidanimations:library:2.3@aar'

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

 

Primeiro, na época da construção deste artigo já havia uma versão do plugin Gradle superior a versão 3.1.4, mais precisamente a versão 3.2.1.

Porém a versão 3.2.1 estável da biblioteca com.android.databinding ainda não estava disponível. Assim optei por dar um downgrade no plugin do Gradle, classpath 'com.android.tools.build:gradle:3.1.4', e manter o uso da versão 3.1.4.

Segundo, caso não haja conflitos de APIs, como houve em meu caso de teste com a nova versão estável do plugin do Gradle, então utilize sempre as versões estáveis mais atuais das APIs e SDKs referenciados em aplicativo.

Terceiro e último ponto, como este artigo já é o terceiro sobre a Data Binding API, então já iniciaremos com as estruturas Data Binding, mesmo sabendo que somente na segunda parte do conteúdo é que faremos uso de uma anotação de binding adapter.

Configurações AndroidManifest

A seguir as configurações do AndroidManifest.xml:

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

<application
android:hardwareAccelerated="true"
android:allowBackup="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=".PIBActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

</application>
</manifest>

Configurações de estilo

Para as definições de estilo, vamos iniciar com a arquivo de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#9E9E9E</color>
<color name="colorPrimaryDark">#616161</color>
<color name="colorAccent">#FFC107</color>

<color name="colorContentBackground">#F0F0F0</color>

<color name="colorSpinnerStroke">#BBBBBB</color>
<color name="colorContentText">#666666</color>

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

<color name="colorBlockPopulation">#EAEAEA</color>
<color name="colorBlockArea">#E3E3E3</color>
<color name="colorBlockIdh">#DCDCDC</color>
<color name="colorBlockPerCapitaIncome">#D4D4D4</color>
<color name="colorBlockIlliteracy">#CACACA</color>
<color name="colorBlockLifeExpectancy">#BDBDBD</color>
</resources>

 

Agora o arquivo de arrays, que na verdade é um arquivo de dados e não de estilo, mas vamos coloca-lo aqui, pois ele está no mesmo folder de conteúdos estáticos do projeto. Segue /res/values/arrays.xml:

<resources>
<string-array name="states">
<item>Acre (AC)</item>
<item>Alagoas (AL)</item>
<item>Amapá (AP)</item>
<item>Amazonas (AM)</item>
<item>Bahia (BA)</item>
<item>Ceará (CE)</item>
<item>Distrito Federal (DF)</item>
<item>Espírito Santo (ES)</item>
<item>Goiás (GO)</item>
<item>Maranhão (MA)</item>
<item>Mato Grosso (MT)</item>
<item>Mato Grosso do Sul (MS)</item>
<item>Minas Gerais (MG)</item>
<item>Pará (PA)</item>
<item>Paraíba (PB)</item>
<item>Paraná (PR)</item>
<item>Pernambuco (PE)</item>
<item>Piauí (PI)</item>
<item>Rio de Janeiro (RJ)</item>
<item>Rio Grande do Norte (RN)</item>
<item>Rio Grande do Sul (RS)</item>
<item>Rondônia (RO)</item>
<item>Roraima (RR)</item>
<item>Santa Catarina (SC)</item>
<item>São Paulo (SP)</item>
<item>Sergipe (SE)</item>
<item>Tocantins (TO)</item>
</string-array>
</resources>

 

Então o arquivo de Strings, /res/values/strings.xml:

<resources>
<string name="app_name">Brasil - PIB Estados</string>

<string name="label_pib">PIB:</string>
<string name="label_population">População</string>
<string name="label_area">Área (em km²)</string>
<string name="label_idh">Índice de Desenvolvimento Humano (IDH)</string>
<string name="label_per_capita_income">Renda per capita</string>
<string name="label_illiteracy">Analfabetismo</string>
<string name="label_life_expectancy">Expectativa de vida ao nascer (anos)</string>
</resources>

 

Por fim o arquivo de definição de estilo, tema, do aplicativo, /res/values/styles.xml:

<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>

<!--
Configurações para Spinner com fundo branco e
texto escuro.
-->
<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">@color/colorContentText</item>
</style>
</resources>

Pacote de domínio

No pacote de domínio, /domain, teremos apenas uma classe, mais precisamente a classe que representa cada estado, State:

class State(
val pib: Double,
val population: String,
val area: String,
val perCapitaIncome: String,
val idh: String,
val illiteracy: String,
val lifeExpectancy: String ){

fun getPibFormatted() =
String.format( Locale.GERMANY, "R\$ %,.3f bilhões (em 2015)", pib )
}

Pacote de dados

Para o pacote de dados, /data, teremos também apenas uma classe, mas essa incluindo alguns algoritmos de processamento da base estática de dados, não somente o fornecimento dos dados.

A classe é uma entidade da categoria mock, dados simulados, estratégia comum para teste de aplicativos em ambiente de desenvolvimento.

Segue classe Database:

class Database {

companion object {

private fun getStates() =
listOf(
State( /* Acre */
13.622,
"869.265 habitantes",
"152.581,388",
"R\$ 16.953,46",
"0,663",
"12,1%",
"73,9"
),
State( /* Alagoas */
46.364,
"3.322.820 habitantes",
"27.767,661",
"R\$ 13.877,00",
"0,631",
"18,2%",
"71,6"
),
State( /* Amapá */
13.861,
"829.494 habitantes",
"142.814,585",
"R\$ 18.079,00",
"0,708",
"5%",
"73,9"
),
State( /* Amazonas */
86.560,
"4.080.611 habitantes",
"1.570.745,680",
"R\$ 21.978,00",
"0,674",
"6,2%",
"71,9"
),
State( /* Bahia */
245.025,
"14.812.617 habitantes",
"564.692,669",
"R\$ 16.115,00",
"0,663",
"12,7%",
"73,5"
),
State( /* Ceará */
130.621,
"9.075.649 habitantes",
"146.348",
"R\$ 14.669,00",
"0,682",
"14,2%",
"73,8"
),
State( /* Distrito Federal */
215.613,
"2.974.703 habitantes",
"5.779,999",
"R\$ 73.971,00",
"0,824",
"2,5%",
"78,1"
),
State( /* Espírito Santo */
120.363,
"3.972.388 habitantes",
"46.077,519",
"R\$ 30.627,00",
"0,740",
"5,5%",
"78,2"
),
State( /* Goiás */
173.632,
"6.921.161 habitantes",
"340.111,376",
"R\$ 26.265,00",
"0,735",
"5,9%",
"74,2"
),
State( /* Maranhão */
78.475,
"7.035.055 habitantes",
"331.983",
"R\$ 11.366,00",
"0,639",
"16,7%",
"70,6"
),
State( /* Mato Grosso do Sul */
83.082,
"2.748.023 habitantes",
"357.145,534",
"R\$ 31.337,00",
"0,729",
"5%",
"75,5"
),
State( /* Mato Grosso */
101.235,
"3.441.998 habitantes",
"903.378,292",
"R\$ 30.137,00",
"0,725",
"6,5%",
"74,2"
),
State( /* Minas Gerais */
519.326,
"21.040.662 habitantes",
"588.528,29",
"R\$ 24.884,00",
"0,731",
"6%",
"77,2"
),
State( /* Pará */
130.883,
"8.513.497 habitantes",
"1.247.689,5",
"R\$ 16.009,00",
"0,646",
"8,6%",
"72,1"
),
State( /* Paraíba */
56.140,
"3.996.496 habitantes",
"56.584,6",
"R\$ 14.133,00",
"0,658",
"16,5%",
"73,2"
),
State( /* Paraná */
376.960,
"11.348.937 habitantes",
"199.307,945",
"R\$ 33.768,00",
"0,749",
"4,6%",
"77,1"
),
State( /* Pernambuco */
156.955,
"9.496.294 habitantes",
"98.311,616",
"R\$ 16.795,00",
"0,673",
"13,4%",
"73,9"
),
State( /* Piauí */
39.148,
"3.264.531 habitantes",
"251.529,186",
"R\$ 12.218,00",
"0,646",
"16,6%",
"71,1"
),
State( /* Rio de Janeiro */
659.137,
"17.159.960 habitantes",
"43.696,054",
"R\$ 39.826,00",
"0,761",
"2,5%",
"76,2"
),
State( /* Rio Grande do Norte */
57.250,
"3.479.010 habitantes",
"52.796,791",
"R\$ 16.631,00",
"0,684",
"13,5%",
"75,7"
),
State( /* Rio Grande do Sul */
381.985,
"11.329.605 habitantes",
"281.731,445",
"R\$ 33.960,00",
"0,746",
"3%",
"77,8"
),
State( /* Rondônia */
36.563,
"1.757.589 habitantes",
"237.576,167",
"R\$ 20.677,00",
"0,690",
"7,2%",
"71,3"
),
State( /* Roraima */
10.354,
"576.568 habitantes",
"224.298",
"R\$ 20.476,00",
"0,707",
"6%",
"71,5"
),
State( /* Santa Catarina */
249.073,
"7.075.494 habitantes",
"95.733,978",
"R\$ 36.525,00",
"0,774",
"2,6%",
"79,1"
),
State( /* São Paulo */
1939.890,
"45.538.936 habitantes",
"248.222,362",
"R\$ 43.694,00",
"0,783",
"2,6%",
"78,1"
),
State( /* Sergipe */
38.554,
"2.278.308 habitantes",
"21.910,348",
"R\$ 17.189,00",
"0,665",
"14,5%",
"72,7"
),
State( /* Tocantins */
28.930,
"1.555.229 habitantes",
"277.620,914",
"R\$ 19.094,00",
"0,699",
"10,2%",
"73,4"
)
)

fun getState( index: Int ) =
getStates()[ index ]

fun getPibBrazil() =
getStates().map{ it.pib }.sum()
}
}

 

companion object está sendo utilizado para que seja mais simples o uso de uma entidade Database, não exigindo assim a criação de uma instância desta classe sempre que algum dos métodos dela for necessário.

Atividade principal, PIBActivity

Para a atividade principal do projeto, vamos iniciar com arquivo de layout, já com as definições necessárias de um arquivo que faz uso da biblioteca Data Binding.

Segue /res/layout/activity_pib.xml:

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

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

<!--
Hackcode o uso de um RelativeLayout somente para conter
o padding interno que deveria vir no ScrollView, porém
se adicionado o padding no ScrollView parte do conteúdo
ficará abaixo da barra de fundo para navegação.
-->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="16dp"
android:background="@drawable/content_background">

<TextView
android:id="@+id/tv_pib_label"
android:layout_marginTop="13dp"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:text="@string/label_pib"/>

<Spinner
android:id="@+id/sp_states"
android:layout_width="wrap_content"
android:layout_height="46dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:layout_toRightOf="@+id/tv_pib_label"
android:layout_toEndOf="@+id/tv_pib_label"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:entries="@array/states"
android:background="@drawable/spinner_background"
android:popupBackground="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme"/>

<View
android:id="@+id/v_vertical_line"
android:layout_width="match_parent"
android:layout_height="0.6dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_below="@+id/sp_states"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:background="@color/colorLine"/>

<TextView
android:id="@+id/tv_pib_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/v_vertical_line"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="center" />

<az.plainpie.PieView
android:id="@+id/pv_pib"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_marginTop="16dp"
android:layout_below="@+id/tv_pib_state"
android:layout_centerHorizontal="true"
plainpie:inner_pie_padding="30"
plainpie:inner_text_visibility="true"/>

<RelativeLayout
android:id="@+id/rl_population"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_below="@+id/pv_pib"
android:padding="16dp"
android:background="@color/colorBlockPopulation">

<TextView
android:id="@+id/tv_population_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:text="@string/label_population"/>

<TextView
android:id="@+id/tv_population_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/tv_population_label"
android:layout_toEndOf="@+id/tv_population_label"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="end" />
</RelativeLayout>

<RelativeLayout
android:id="@+id/rl_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/rl_population"
android:padding="16dp"
android:background="@color/colorBlockArea">

<TextView
android:id="@+id/tv_area_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:text="@string/label_area"/>

<TextView
android:id="@+id/tv_area_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/tv_area_label"
android:layout_toEndOf="@+id/tv_area_label"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="end" />
</RelativeLayout>

<RelativeLayout
android:id="@+id/rl_idh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/rl_area"
android:padding="16dp"
android:background="@color/colorBlockIdh">

<TextView
android:id="@+id/tv_idh_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:text="@string/label_idh"/>

<TextView
android:id="@+id/tv_idh_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/tv_idh_label"
android:layout_toEndOf="@+id/tv_idh_label"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="end" />
</RelativeLayout>

<RelativeLayout
android:id="@+id/rl_per_capita_income"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/rl_idh"
android:padding="16dp"
android:background="@color/colorBlockPerCapitaIncome">

<TextView
android:id="@+id/tv_per_capita_income_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:text="@string/label_per_capita_income"/>

<TextView
android:id="@+id/tv_per_capita_income_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/tv_per_capita_income_label"
android:layout_toEndOf="@+id/tv_per_capita_income_label"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="end" />
</RelativeLayout>

<RelativeLayout
android:id="@+id/rl_illiteracy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/rl_per_capita_income"
android:padding="16dp"
android:background="@color/colorBlockIlliteracy">

<TextView
android:id="@+id/tv_illiteracy_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:text="@string/label_illiteracy"/>

<TextView
android:id="@+id/tv_illiteracy_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/tv_illiteracy_label"
android:layout_toEndOf="@+id/tv_illiteracy_label"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="end" />
</RelativeLayout>

<RelativeLayout
android:id="@+id/rl_life_expectancy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_below="@+id/rl_illiteracy"
android:background="@drawable/last_content_item_background">

<TextView
android:id="@+id/tv_life_expectancy_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:text="@string/label_life_expectancy"/>

<TextView
android:id="@+id/tv_life_expectancy_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/tv_life_expectancy_label"
android:layout_toEndOf="@+id/tv_life_expectancy_label"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:gravity="end" />
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
</ScrollView>
</layout>

 

Agora o arquivo drawable que permite bordas arredondadas no segundo RelativeLayout do layout. Segue /res/drawable/content_background.xml:

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

<!--
Definindo cor de background e a curvatura de pontas.
-->
<solid android:color="@color/colorContentBackground"/>

<corners android:radius="5dp"/>
</shape>

 

Com o drawable acima temos o resultado a seguir (note os cantos arredondados em cinza claro):

Cantos arredondados em cinza claro

Assim o arquivo drawable que é o background do Spinner. Segue /res/drawable/spinner_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/white" />

<corners android:radius="3dp" />

<stroke
android:width="0.8dp"
android:color="@color/colorSpinnerStroke" />
</shape>
</item>

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

 

Com o drawable anterior temos o seguinte resultado:

Spinner Android com background customizado

Então o arquivo drawable que permite que o último item informativo tenha também as bordas inferiores arredondadas para seguir o modelo do RelativeLayout container. Segue /res/drawable/last_content_item_background.xml:

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

<!--
Definindo cor de background e a curvatura de
pontas inferiores.
-->
<solid android:color="@color/colorBlockLifeExpectancy"/>

<corners android:bottomLeftRadius="5dp" android:bottomRightRadius="5dp"/>
</shape>

 

Com o drawable anterior conseguimos o seguinte resultado:

Bordas inferiores com cantos arredondados

A seguir o diagrama do layout activity_pib.xml:

Diagrama do layout activity_pib.xml

Agora o código Kotlin de PIBActivity:

class PIBActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityPibBinding>(
this,
R.layout.activity_pib
)

configPieChart()
}

private fun configPieChart(){
/*
* Alterando o tamanho do texto do widget.
* */
pv_pib.setPercentageTextSize( 35.0F )

/*
* Alterando o preenchimento de cor da barra que representa
* a porcentagem atual.
* */
pv_pib.setPercentageBackgroundColor(
ContextCompat.getColor( this, R.color.colorAccent )
)
}

private fun pieChartNewValue( state: State ){
/*
* Obtendo a porcentagem do PIB do estado escolhido
* em relação ao PIB nacional.
* */
val percentage = (state.pib / Database.getPibBrazil()) * 100

pv_pib.setInnerText( String.format("%.1f", percentage) )
pv_pib.percentage = percentage.toFloat()

val animation = PieAngleAnimation( pv_pib )
animation.duration = 800 /* Duração da animação, em milissegundos. */
pv_pib.startAnimation( animation )
}

fun updateScreen( index: Int ){
val state = Database.getState( index )

pieChartNewValue( state )

tv_pib_state.text = state.getPibFormatted()
animationView( tv_pib_state )

tv_population_value.text = state.population
animationView( tv_population_value )

tv_area_value.text = state.area
animationView( tv_area_value )

tv_idh_value.text = state.idh
animationView( tv_idh_value )

tv_per_capita_income_value.text = state.perCapitaIncome
animationView( tv_per_capita_income_value )

tv_illiteracy_value.text = state.illiteracy
animationView( tv_illiteracy_value )

tv_life_expectancy_value.text = state.lifeExpectancy
animationView( tv_life_expectancy_value )
}

private fun animationView( view: View ){
YoYo
.with( Techniques.FlipInX )
.duration( 1300 )
.playOn( view )
}
}

 

Assim podemos partir para a melhoria, colocando o listener de item selecionado no Spinner, isso via @BindingMethods, proporcionando uma melhor separação do código.

Atualização de projeto, ouvidor de item selecionado via @BindingMethods

Sem o listener de item selecionado, o que temos atualmente é um aplicativo 100% estático, como a seguir:

Animação do app Android Brasil - PIB Estados sem listener vinculado

Há várias maneiras de adicionarmos ao Spinner o listener de item selecionado, mas aqui vamos optar pelo uso do @BindingMethods junto ao atributo android:onItemSelected com o propósito de melhorar a leitura do código XML e também de separar o código listener em uma subclasse Spinner.

Nova classe Spinner, já com o código listener

Nosso primeiro passo é criar uma nova classe Spinner já com o algoritmo de vinculação de listener de item selecionado.

Primeiro crie um pacote /view, depois adicione a este novo pacote a classe SpinnerWithListener como a seguir:

@BindingMethods(
value = [
BindingMethod(
type = Spinner::class,
attribute = "android:onItemSelected",
method = "setSpinnerItemSelected"
)
]
)
class SpinnerWithListener: Spinner {

/*
* Sobrescrevendo os dois construtores obrigatórios para
* que seja possível a criação de uma subclasse de uma
* visualização.
* */
constructor( context: Context ) : super( context ) {}
constructor( context: Context, attrs: AttributeSet ) : super( context, attrs ) {}

/*
* A atividade principal do projeto entra como argumento,
* pois é ela que contém o método de atualização dos
* componentes visuais em tela, o método updateScreen().
* */
fun setSpinnerItemSelected(
activity: PIBActivity
){

val listener = object: AdapterView.OnItemSelectedListener{

override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
activity.updateScreen( position )
}

override fun onNothingSelected( parent: AdapterView<*>? ) {
/*
* Quando nada foi selecionado ainda, os dados
* do primeiro estado. Acre, são requisitados.
* */
activity.updateScreen( 0 )
}
}

this.onItemSelectedListener = listener
}
}

 

Você deve ter notado o atributo android:onItemSelected, este que somente é incluído no framework Android quando a biblioteca Data Binding é liberada em projeto.

Essa é uma das características da API Data Binding, alguns atributos e Interfaces são adicionados ao projeto para representarem também os métodos das Interfaces listeners do Android. Exemplo:

  • Para a Interface AdapterView.OnItemSelectedListener são adicionadas as seguintes novas Interfaces e atributos:
    • AdapterViewBindingAdapter.OnItemSelected e android:onItemSelected;
    • AdapterViewBindingAdapter.OnNothingSelected e android:onNothingSelected.

Principalmente os atributos adicionados é que facilitam a leitura dos códigos XML, pois assim fica explicito o listener adicionado a View. Tendo em mente que é comum utilizarmos listeners onde muitos dos métodos obrigatórios não são necessários.

Preparando o layout com a nova View e atributo onItemSelected

A atualização no layout /res/layout/activity_pib.xml é simples. A seguir temos os trechos atualizados / adicionados:

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

<data>
<!--
Para que seja possível a aplicação do cast na propriedade
context que já é disponibilizada pela Data Binding library.
-->
<import type="thiengo.com.br.brasil_pibestados.PIBActivity" />
</data>

<ScrollView ...>

...
<RelativeLayout ...>

<RelativeLayout ...>
...

<thiengo.com.br.brasil_pibestados.view.SpinnerWithListener
android:onItemSelected="@{ (PIBActivity) context }"
... />
...
</RelativeLayout>
</RelativeLayout>
</ScrollView>
</layout>

 

Assim podemos partir para os testes.

Testes e resultados

No Android Studio vá em "Build", então em "Rebuid project". Ao final do rebuild execute o aplicativo em seu aparelho ou emulador Android de testes.

Aplicando algumas atualizações no SpinnerWithListener, temos:

Animação completa do app Android Brasil - PIB Estados

Assim finalizamos mais um importante conteúdo sobre a biblioteca Data Binding.

Não deixe de se inscrever na 📩 lista de emails do Blog para receber em primeira mão os conteúdos exclusivos sobre desenvolvimento Android.

Se inscreva no canal do Blog em: YouTube Thiengo.

Slides

Abaixo os slides com a explicação dos binding adapters da API Data Binding:

Vídeos

A seguir os vídeos com o passo a passo da adição do listener de item selecionado ao Spinner do aplicativo Android de dados estaduais:

Para acessar o projeto de exemplo acesse o GitHub dele em: https://github.com/viniciusthiengo/brasil-pib-estados-kotlin-android.

Conclusão

Como informado na primeira parte do artigo, binding adapters terão seu beneficio reconhecido em pontos específicos de qualquer domínio de problema sendo trabalhado em aplicativos Android, isso, pois o uso das anotações @Binding não é algo comum, tendo em mente que a biblioteca Data Binding é ainda maior.

Um ponto importante e que deve ser levado a sério é a eficaz documentação dos métodos binding, pois esses não exigem referências explícitas em código XML.

Assim finalizamos o conteúdo. Caso você tenha alguma dica ou dúvida sobre Data Binding no Android, deixe nos comentários.

Se curtiu o artigo, não esqueça de compartilha-lo. E por fim, não deixe de se inscrever na 📩 lista de emails.

Abraço.

Fontes

Binding adapters

Data bindings with custom listeners on custom view - Resposta de subhash

Só Geografia - Estados brasileiros

Sua pesquisa - Estados Brasileiros

Kotlin sum() and sumBy() method for List, Map of Objects example

Objects (aka Easily Create Singletons)

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

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
Observable Binding Para Atualização na UI AndroidObservable Binding Para Atualização na UI AndroidAndroid
Ajuste de Texto com Autosizing TextView - Android JetpackAjuste de Texto com Autosizing TextView - Android JetpackAndroid

Compartilhar

Comentários Facebook

Comentários Blog

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...