Utilizando o conceito de two-way data binding no Android

Apesar de ser um conceito antigo no desenvolvimento de aplicações móveis nativas no Android, infelizmente ainda vemos nos dias de hoje muitos desenvolvedores que desconhecem uma poderosa forma de minimizar a quantidade de código a ser escrito no desenvolvimento de aplicações nativas Android, no que diz respeito ao bind entre elementos das interfaces (XML’s) e da sua implementação de negócio (classes Java).

Para exemplificarmos a utilização do two-way data binding, iniciamos a criação de um projeto em seu Android Studio, e utilizamos o template de uma Activity vazia ao projeto, conforme imagem abaixo:

Para esse exemplo, iremos utilizar o template de projeto “Empty Activity”

Neste artigo iremos trabalhar com uma abordagem de comparação entre o “old school” e o “new school“, com o objetivo de termos uma visão real dos ganhos nesta proposta de implementação.

Após a criação do projeto, o primeiro passo é a edição da nossa activity principal, aonde iremos adicionar um campo para que o nosso usuário possa imputar um texto qualquer e um botão para que possamos capturar o toque de um usuário:

Adicionado o EditText e Button a nossa Activity

Como resultado de código obtemos a seguinte implementação:

[code lang=”xml”]
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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="br.com.kepha.opensource.twowaydatabind.MainActivity">

<EditText
android:id="@+id/editText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/editText" />

</android.support.constraint.ConstraintLayout>
[/code]

O próximo passo, será adicionarmos a nossa implementação a captura do evento de toque no botão, que irá enviar para o usuário uma mensagem de alerta com o texto informado no EditText. Para esse resultado, efetuamos a seguinte implementação:

[code lang=”java”]
package br.com.kepha.opensource.twowaydatabind;

import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

public class MainActivity extends AppCompatActivity {

/**
* Button
*/
private Button button;

/**
* EditText
*/
private EditText editText;

@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

// Definição do layout
setContentView(R.layout.activity_main);

// Bind dos objetos da interface
editText = (EditText) findViewById(R.id.editText);
button = (Button) findViewById(R.id.button);

// Evento de clique do botão
button.setOnClickListener(new View.OnClickListener(){
public void onClick(View view) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("Alerta");
builder.setMessage(editText.getText());
AlertDialog dialog = builder.create();
dialog.show();
}
});

}

}
[/code]

Nesta implementação, devemos nos atentar as linhas 28, 31 e 32 do nosso código. Nestes pontos, nós estamos informando ao Android, que o layout da Activity que deve ser utilizado é o layout “activity_main” e que os objetos EditText e Button devem fazer referências aos elementos com os id’s “editText” e “button” respectivamente, indicados através da classe R, que em um projeto Android é responsável por armazenar todos os elementos de layouts.

Um grande problema desta implementação é que caso ocorra a alteração de algum atributo “id” durante a construção dos layouts (arquivos XML’s), obrigatoriamente você terá que efetuar um refactory em sua implementação Java para efetuar a correta referência de objetos.

Observando um projeto pequeno como este, este processo pode parecer despreocupante, mas infelizmente em projetos com um volume muito grande de telas e consequentemente um grande número de elementos visuais, fica evidente a complexidade da gestão de nomes de atributos e sua referência em classes Java.

Um desafio é reduzirmos esta complexidade e consequentemente termos um código mais simples, objetivo e com uma melhor manutenibilidade.

A adoção da técnica de two-way databind facilita a nossa implementação, reforçando todos os aspectos citados. Justificado o motivo da adoção, daremos inicio a configuração do nosso projeto para este objetivo.

Primeiramente, no arquivo que controla as informações da build do módulo do nosso projeto (build.gradle), precisamos adicionar o suporte ao databind conforme a instrução abaixo (linhas 21 a 23):

[code lang=”java”]
apply plugin: ‘com.android.application’

android {
compileSdkVersion 26
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "br.com.kepha.opensource.twowaydatabind"
minSdkVersion 24
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
}
}
// Suporte ao databing
dataBinding {
enabled = true
}
}

dependencies {
compile fileTree(dir: ‘libs’, include: [‘*.jar’])
androidTestCompile(‘com.android.support.test.espresso:espresso-core:2.2.2’, {
exclude group: ‘com.android.support’, module: ‘support-annotations’
})
compile ‘com.android.support:appcompat-v7:26.+’
compile ‘com.android.support.constraint:constraint-layout:1.0.2’
testCompile ‘junit:junit:4.12’
}
[/code]

Após essa alteração, fique atento a mensagem que o Android Studio apresenta sobre a necessidade da sincronização do projeto com a IDE, para que ela trabalhe de forma correta devido as alterações. Isso é necessário porque a partir deste momento, novas classes serão criadas dinamicamente para fazermos as referências sobre o binding, mas isso será apresentado de forma mais clara na sequência do artigo.

Sync – Android Studio

Terminada a configuração do projeto e do Android Studio, o próximo passo é criarmos um objeto Model para que armazene as informações da nossa View. Para este exemplo básico, iremos criar uma classe Java, chamada Mensagem, com um único atributo para armazenarmos a mensagem, conforme abaixo:

[code lang=”java”]
package br.com.kepha.opensource.twowaydatabind.model;

public class Mensagem {

private String descricao;

public String getDescricao() {
return descricao;
}

public void setDescricao(String descricao) {
this.descricao = descricao;
}

public Mensagem() {

}

public Mensagem(String descricao) {
this.descricao = descricao;
}

}
[/code]

O próximo passo indica uma das mudanças com maior impacto nessa proposta de arquitetura no que diz respeito a estrutura dos arquivos de layout da sua aplicação. Para uma arquitetura utilizando two-way databinding é necessário o seguinte refactory:

  • A tag raíz do seu arquivo xml passará a ser a tag: layout
  • No primeiro nível da estrutura xml, iremos adicionar uma tag chamada data aonde iremos indicar nela os objetos que darão suporte ao databind
  • Dentro da tag data, adicionamos uma nova tag chamada variable, aonde referênciamos um objeto no atributo name, e seu respectivo tipo no atributo type

O refactory do projeto atual ficará da seguinte forma:

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

<data>
<variable name="model" type="br.com.kepha.opensource.twowaydatabind.model.Mensagem"/>
</data>

<android.support.constraint.ConstraintLayout
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="br.com.kepha.opensource.twowaydatabind.MainActivity">

<EditText
android:id="@+id/editText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPersonName"
android:text="@={model.descricao}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/editText" />

</android.support.constraint.ConstraintLayout>

</layout>
[/code]

Devemos nos atentar ao detalhe que a linha 21 também sofreu uma alteração. Primeiramente era informado no atributo android:text um texto qualquer (no caso, Name), porém, agora indicamos que o texto atribuído ao EditText deverá ser o valor contido no objeto Mensagem, no atributo descricao.

Antes:

[code lang=”xml”]
<EditText

android:text="Name"
… />
[/code]

Depois:

[code lang=”xml”]
<EditText

android:text="@={model.descricao}"
… />
[/code]

Feito isso, já é possível efetuarmos as alterações em nossa MainActivity. Essas alterações dirão respeito a forma de atribuição de layouts e objetos que serão feitos o databinding.

Para cada arquivo de layout criado em seu projeto, o AndroidStudio automaticamente irá gerar uma classe para as configurações de databinding (na mesma idéia da classe R). O nome da classe segue sempre um padrão: o nome do layout, adicionando a palavra Binding ao seu final. Neste exemplo, como o nome do nosso arquivo xml é “activity_main“, o AndroidStudio irá adicionar uma classe ao nosso projeto chamada ActivityMainBinding.

Mas qual o conteúdo desta classe? Esta classe terá métodos acessores para os atributos adicionados na tag data.

O resultado da nossa implementação:

[code lang=”java”]
package br.com.kepha.opensource.twowaydatabind;

import android.databinding.DataBindingUtil;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import br.com.kepha.opensource.twowaydatabind.databinding.ActivityMainBinding;
import br.com.kepha.opensource.twowaydatabind.model.Mensagem;

public class MainActivity extends AppCompatActivity {

/**
* Button
*/
private Button button;

/**
* EditText
*/
private EditText editText;

/**
* Mensagem
*/
private Mensagem mensagem = new Mensagem("Mensagem de teste");

/**
* ActivityMainBinding
*/
private ActivityMainBinding activityMainBinding;

@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
activityMainBinding.setModel(this.mensagem);

// Bind dos objetos da interface
button = (Button) findViewById(R.id.button);

// Evento de clique do botão
button.setOnClickListener(new View.OnClickListener(){
public void onClick(View view) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("Alerta");
builder.setMessage(mensagem.getDescricao());
AlertDialog dialog = builder.create();
dialog.show();
}
});

}

}
[/code]

Os detalhes desta implementação ficam os seguintes:

  • Linha 29: Adicionado construtor para o objeto Mensagem.
  • Linha 34: Declaração do objeto ActivityMainBinding.
  • Linha 41: Atribuição de layout.
  • Linha 42: Atribuição do objeto Mensagem ao objeto ActivityMainBinding.
  • Linha 52: Obtenção da mensagem informada no EditText através do objeto Mensagem.

Excelente, neste momento já podemos compilar nosso projeto e teremos o seguinte resultado:

Porém, nossa implementação ainda não está completa. Neste momento, qualquer alteração que ocorra em nosso EditText, será repassada automaticamente para o atributo descricao no objeto Mensagem, entretanto, caso o objeto sofra alguma alteração, a mesma não será refletida em nosso EditText, conforme o exemplo abaixo:

[code lang=”java”]
package br.com.kepha.opensource.twowaydatabind;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {

// Evento de clique do botão
button.setOnClickListener(new View.OnClickListener(){
public void onClick(View view) {
mensagem.setDescricao("Nova mensagem");
}
});

}

}
[/code]

Caso execute o código acima, nenhuma alteração ocorrerá em nosso EditText. Para corrigirmos esta ineficiência, devemos alterar nossa classe Mensagem, conforme abaixo:

[code lang=”java”]
package br.com.kepha.opensource.twowaydatabind.model;

import android.databinding.BaseObservable;
import android.databinding.Bindable;

import br.com.kepha.opensource.twowaydatabind.BR;

public class Mensagem extends BaseObservable {

private String descricao;

@Bindable
public String getDescricao() {
return descricao;
}

public void setDescricao(String descricao) {
this.descricao = descricao;
notifyPropertyChanged(BR.descricao);
}

public Mensagem() {

}

public Mensagem(String descricao) {
this.descricao = descricao;
}

}
[/code]

Os detalhes desta implementação foram os seguintes:

  • Linha 8: Adicionado a herança da classe BaseObservable.
  • Linha 12: Adicionado a declaração da annotation @Bindable.
  • Linha 19: Adicionado a propagação de evento em nosso setter.

Vamos entender cada um dos casos:

BaseObservable

A classe BaseObservable implementa a interface Observable. Essa interface permite que nossa UI possa ser notificada caso ocorra alguma alteração de algum atributo em objetos que ela esteja conectada e fazendo sua observação.

Pelo simples fato de efetuarmos esta herança, nossa classe Mensagem passa a ter acesso aos métodos notifyPropertyChanged() e notifyChange(), que devem ser executados nos momentos em que seja necessário a propagação de notificações sobre mudanças de estados nos objetos.

@Bindable

Sempre que estendemos uma classe que implemente Observable, devemos utilizar a annotation @Bindable em métodos acessores (getters) de atributos que serão referenciados em algum elemento de nossa UI.

Com a declaração da annotation @Bindable, o Android automaticamente adiciona o atributo referenciado ao método getter na classe BR. Essa classe é utilizada para a notificação de qual atributo recebeu alguma alteração e deve ser atualizado na interface.

notifyPropertyChanged

Por fim, em nosso método setter do atributo que recebeu alguma alteração, devemos acionar o método notifyPropertyChanged(), referenciando como parametro qual atributo dentro da nossa classe BR (atualizada dinâmicamente pela adição da annotation @Bindable em nosso método getter) foi alterado, para que nossa UI seja atualizada corretamente.

Com essa implementação, não há mais a necessidade de referenciar em nossa MainActivity elementos visuais da nossa interface, removendo por completo qualquer referência ao EditText, entretanto, ainda possuímos uma referência do objeto Button na nossa MainActivity, para controlarmos o clique do botão e o alerta que deve ser disparado.

Para melhorarmos esta implementação, podemos adicionar uma nova classe ao nosso projeto, que será responsável por controlar os eventos da nossa Activity. Logo abaixo, veremos a classe MainActivityHandlers, que possui o método onButtonClick(View view, Mensagem mensagem):

[code lang=”java”]
package br.com.kepha.opensource.twowaydatabind.handlers;

import android.support.v7.app.AlertDialog;
import android.view.View;

import br.com.kepha.opensource.twowaydatabind.model.Mensagem;

public class MainActivityHandlers {

public void onButtonClick(View view, Mensagem mensagem) {
AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
builder.setTitle("Alerta");
builder.setMessage(mensagem.getDescricao());
AlertDialog dialog = builder.create();
dialog.show();
}

}
[/code]

O método onButtonClick recebeu como parametros nossa View, para que fosse possível obter o Context para o AlertDialog, e também um segundo paramêtro, que foi o objeto Mensagem, que possui o conteúdo que deve ser apresentado no corpo do alerta.

Para que o evento fosse disparado corretamente, foi necessária as seguintes alterações no XML da nossa Activity:

Primeiro, adicionamos um novo atributo variable ao nó data, fazendo uma referencia a nossa classe MainActivityHandlers:

[code lang=”xml”]
<?xml version="1.0" encoding="utf-8"?>
….
<data>
<variable
name="model"
type="br.com.kepha.opensource.twowaydatabind.model.Mensagem"></variable>
<variable
name="handlers"
type="br.com.kepha.opensource.twowaydatabind.handlers.MainActivityHandlers"></variable>
</data>
….

[/code]

Segundo, alteramos o evento de clique em nosso Button, conforme abaixo:

[code lang=”xml”]
<Button

android:onClick="@{(view) -> handlers.onButtonClick(view, model)}"

/>
[/code]

E por fim, efetuamos um grande refactory em nossa classe principal (MainActivity), conforme abaixo:

[code lang=”java”]
package br.com.kepha.opensource.twowaydatabind;

import android.databinding.DataBindingUtil;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import br.com.kepha.opensource.twowaydatabind.databinding.ActivityMainBinding;
import br.com.kepha.opensource.twowaydatabind.handlers.MainActivityHandlers;
import br.com.kepha.opensource.twowaydatabind.model.Mensagem;

public class MainActivity extends AppCompatActivity {

/**
* Mensagem
*/
private Mensagem mensagem = new Mensagem("Mensagem de teste");

/**
* MainActivityHandlers
*/
private MainActivityHandlers mainActivityHandlers = new MainActivityHandlers();

/**
* ActivityMainBinding
*/
private ActivityMainBinding activityMainBinding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
activityMainBinding.setModel(this.mensagem);
activityMainBinding.setHandlers(this.mainActivityHandlers);
}

}
[/code]

Os detalhes desta implementação foram os seguintes:

  • Linha 16: Declaração do objeto Mensagem atributo ao atributo model em nosso XML.
  • Linha 21: Declaração do objeto MainActivityHandlers atributo ao atributo handlers em nosso XML.
  • Linha 32: Atribuição do objeto Mensagem ao ActivityMainBinding.
  • Linha 33: Atribuição do objeto MainActivityHandlers ao ActivityMainBinding.

Um grande ganho na utilização do two-way databind está relacionado a uma melhoria na forma de programar, entretanto, isso não significa um menor número de linhas programáveis, mas sim, uma melhor definição de papeis dentro do seu projeto, garantindo um maior índice de manutenibilidade do seu código 🙂

O código deste projeto está disponível em nosso github.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *