Observable Binding Para Atualização na UI Android
(4857)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo vamos ao estudo dos tipos observáveis da biblioteca Data Binding. Você notará a facilidade de atualização dos elementos em tela quando utilizando esses tipos.
Como projeto de exemplo construiremos a área de contatos de um aplicativo de mensagens, área que se beneficiará do uso de tipos observáveis:
Antes de continuar, caso você ainda não conheça a biblioteca Data Binding, é importante que primeiro estude o artigo Data Binding Para Vinculo de Dados na UI Android.
Aqui o termo "API" será utilizado como sinônimo de "biblioteca" (library).
Antes de prosseguir, não esqueça de se inscrever 📩 na lista de e-mails do Blog para ter acesso aos conteúdos exclusivos.
A seguir os tópicos que estaremos estudando:
- Para que os tipos observáveis de Data Binding?:
- Instalação da biblioteca;
- Classes de domínio e layout ainda sem tipos observáveis;
- Tipos observáveis de campo;
- Colocando tipos observáveis de campo no código de exemplo;
- Coleções observáveis: ObservableArrayMap;
- Coleções observáveis: ObservableArrayList;
- Objeto observável;
- Tipos observáveis em adaptadores de listas;
- Pontos negativos;
- Ponto positivo;
- Considerações finais.
- Projeto Android:
- Protótipo estático;
- Iniciando o projeto;
- Configurações Gradle;
- Configurações AndroidManifest;
- Configurações de estilo;
- Classes de domínio;
- Base de dados simulados;
- Classe adaptadora de contatos;
- Atividade principal, listagem de contatos;
- LocalBroadcast para transporte de mensagem push;
- Application para simulação de notificação push.
- Atualização de projeto, colocando tipos observáveis:
- Slides;
- Vídeos;
- Conclusão;
- Fontes.
Para que os tipos observáveis de Data Binding?
Quando trabalhando em projeto Android quase sempre temos de ter o trecho de código responsável por atualizar as visualizações em tela, isso, pois os objetos com os dados referenciados nas visualizações já foram atualizados e essa atualização precisa ser refletida ao usuário do aplicativo.
Com os tipos observáveis da biblioteca Data Binding nós, desenvolvedores, não mais precisamos nos preocupar com essa parte do fluxo: invocações de métodos e visualizações para refletir a atualização de objetos também em tela.
Há três tipos de classes observáveis dentro da Data Binding API:
- Observáveis de campo (propriedade);
- Observáveis de coleções;
- Observáveis de objetos.
Já lhe adianto que os observáveis de campo quase sempre serão os utilizados em desenvolvimento. Com isso podemos partir para os códigos de exemplo.
Instalação da biblioteca
No Kotlin a configuração de instalação da biblioteca Data Binding exige as seguintes referências 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 artigo 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"
...
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
}
}
...
Classes de domínio e layout ainda sem tipos observáveis
A seguir alguns protótipos de classes e layout ainda sem uso dos tipos observáveis, mas já trabalhando a sintaxe Data Binding.
Primeiro a classe Brand:
class Brand(
val name : String,
val since : Int
)
Agora a classe Car:
class Car(
val model : String,
val brand : Brand,
val year : Int,
val engineStrength : Double
)
Então o layout car.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.Car" />
<variable
name="car"
type="Car" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Modelo: %s", car.model) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Marca: %s (%d)", car.brand.name, car.brand.since) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Ano: %d", car.year) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Motor: %.1f", car.engineStrength) }' />
</LinearLayout>
</layout>
Todos os códigos apresentados funcionam sem problemas, mas em caso de alguma das propriedades tanto de Brand quanto de Car ser atualizada, ainda é preciso acessar a View a qual o propriedade está vinculada e então colocar o novo valor para termos o reflexo da atualização também em tela.
Tipos observáveis de campo
Os tipos observáveis de campo são os seguintes:
- ObservableBoolean;
- ObservableByte;
- ObservableChar;
- ObservableShort;
- ObservableInt;
- ObservableLong;
- ObservableFloat;
- ObservableDouble;
- ObservableParcelable;
- ObservableField.
O termo "observáveis de campo" até mesmo aparenta que estaremos com um código listener para dados de entrada do usuário, em um EditText, por exemplo.
Mas na verdade não, o "campo" é de propriedade, ou variável de instância.
Colocando tipos observáveis de campo no código de exemplo
Para que as atualizações em objetos dos tipos Brand e Car sejam refletidas também em tela sem a necessidade de códigos de acesso a Views por parte do desenvolvedor poderíamos primeiro ter a seguinte nova classe Brand:
class Brand(
val name: ObservableField<String>,
val since: ObservableInt
)
E então a seguinte nova classe Car:
class Car(
val model : ObservableField<String>,
val brand : Brand,
val year : ObservableInt,
val engineStrength : ObservableDouble
)
A documentação de tipos observáveis Data Binding informa para sempre definirmos esses tipos como somente de leitura, val. Poderemos utilizar o método set() para atualizar o valor dentro do tipo observável.
Se um novo objeto observável for setado na propriedade, a igualdade de referência em layout será perdida e assim a atualização não ocorrerá em tela, pois a propriedade em código dinâmico e a referência em layout não mais apontam para o mesmo objeto observável.
Agora um pequeno algoritmo de atualização de dados em objetos, somente para teste de atualização também em tela:
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
val binding = DataBindingUtil.setContentView<CarBinding>( this, R.layout.car )
val car = Car(
ObservableField( "Palio" ),
Brand(
ObservableField( "FIAT" ),
ObservableInt( 1922 )
),
ObservableInt( 2015 ),
ObservableDouble( 1.0 )
)
binding.car = car
Thread{
kotlin.run {
/*
* Aplicando um delay para que seja possível
* visualizar a atualização da UI de acordo
* com a mudança de valor no objeto vinculado
* a ela.
* */
SystemClock.sleep( 3000 )
car.model.set( "Cruze" )
car.year.set( 2018 )
car.engineStrength.set( 2.0 )
car.brand.name.set( "Chevrolet" )
car.brand.since.set( 1911 )
}
}.start()
}
}
Note que quando trabalhando em código dinâmico, para acessar os valores dos tipos observáveis temos de utilizar os métodos get() e set(), respectivamente para acesso e atualização de valores.
No layout XML tudo continua do mesmo jeito, não há necessidade de invocar nem mesmo o método get().
Executando o projeto com os novos códigos observáveis, temos:
Coleções observáveis: ObservableArrayMap
Há dois tipos de coleções observáveis, vamos iniciar com a ObservableArrayMap. A seguir um código trabalhando com uma coleção de objetos do tipo Car:
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
val binding = DataBindingUtil.setContentView<CarBinding>( this, R.layout.car )
val car = Car(
ObservableField( "Toro" ),
Brand(
ObservableField( "FIAT" ),
ObservableInt( 1922 )
),
ObservableInt( 2018 ),
ObservableDouble( 2.0 )
)
val carMap = ObservableArrayMap<String, Car>()
carMap.put( "car_key", car )
binding.carMap = carMap
Thread{
kotlin.run {
/*
* Aplicando um delay para que seja possível
* visualizar a atualização da UI de acordo
* com a mudança de valor no objeto vinculado
* a ela.
* */
SystemClock.sleep( 3000 )
val carStep = carMap.get( "car_key" )
carStep!!.model.set( "Saveiro" )
carStep.year.set( 2017 )
carStep.engineStrength.set( 1.6 )
carStep.brand.name.set( "Volkswagen" )
carStep.brand.since.set( 1932 )
}
}.start()
}
}
Então o layout car.xml atualizado:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="thiengo.com.br.databindingtests.Car" />
<import type="android.databinding.ObservableArrayMap" />
<variable
name="carMap"
type="ObservableArrayMap<String, Car>" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Modelo: %s", carMap.car_key.model) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Marca: %s (%d)", carMap["car_key"].brand.name, carMap.car_key.brand.since) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Ano: %d", carMap["car_key"].year) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Motor: %.1f", carMap.car_key.engineStrength) }' />
</LinearLayout>
</layout>
No layout anterior é possível notar que há diferentes modos de acesso aos dados, podemos trabalhar com o modelo convencional, map["chave_de_acesso_ao_valor"], ou no modelo também aceito pela sintaxe Data Binding, map.chave_de_acesso_ao_valor.
Também note a representação de ObservableArrayMap<String, Car> em sintaxe Data Binding: ObservableArrayMap<String, Car>. Isso, pois os sinais > e < não são aceitos como valores na sintaxe desta biblioteca.
Executando o projeto com o código anterior, temos:
Coleções observáveis: ObservableArrayList
O outro tipo de coleção observável é o ObservableArrayList, um tipo mais próximo do que poderíamos utilizar, por exemplo, junto a um adaptador de framework de lista, framework como o RecyclerView.
A seguir um código com um objeto ObservableArrayList sendo utilizado para conter um objeto Car:
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
val binding = DataBindingUtil.setContentView<CarBinding>( this, R.layout.car )
val car = Car(
ObservableField( "320i" ),
Brand(
ObservableField( "BMW" ),
ObservableInt( 1916 )
),
ObservableInt( 2016 ),
ObservableDouble( 2.4 )
)
val carList = ObservableArrayList<Car>()
carList.add( car )
binding.carList = carList
Thread{
kotlin.run {
/*
* Aplicando um delay para que seja possível
* visualizar a atualização da UI de acordo
* com a mudança de valor no objeto vinculado
* a ela.
* */
SystemClock.sleep( 3000 )
val carStep = carList.get( 0 )
carStep.model.set( "I-Pace" )
carStep.year.set( 2018 )
carStep.engineStrength.set( 3.8 )
carStep.brand.name.set( "Jaguar" )
carStep.brand.since.set( 1922 )
}
}.start()
}
}
Assim o layout car.xml atualizado:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="thiengo.com.br.databindingtests.Car" />
<import type="android.databinding.ObservableArrayList" />
<variable
name="carList"
type="ObservableArrayList<Car>" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Modelo: %s", carList[0].model) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Marca: %s (%d)", carList.get(0).brand.name, carList[0].brand.since) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Ano: %d", carList[0].year) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Motor: %.1f", carList.get(0).engineStrength) }' />
</LinearLayout>
</layout>
Com ObservableArrayList podemos acessar os valores por meio do método get() ou utilizando [].
Você não precisa utilizar números ou valores de chave diretamente em código XML como fizemos no exemplo acima e no exemplo com ObservableArrayMap. Você pode colocar as chaves como valores estáticos de uma classe e então acessar essas chaves com os rótulos definidos em classe, assim o código fica até mais simples de entender.
Executando o projeto com o novo código, temos:
Objeto observável
A outra maneira de se trabalhar com um tipo observável é herdando da classe BaseObservable e então implementando as propriedades observáveis da maneira correta.
A seguir a atualização da classe Car:
class Car(
m: String,
b: Brand,
y: Int,
es: Double ): BaseObservable() {
@get:Bindable
var model : String = ""
set( value ){
field = value
notifyPropertyChanged( BR.model )
}
@get:Bindable
var brand : Brand? = null
set( value ){
field = value
notifyPropertyChanged( BR.brand )
}
@get:Bindable
var year : Int = 0
set( value ){
field = value
notifyPropertyChanged( BR.year )
}
@get:Bindable
var engineStrength : Double = 0.0
set( value ){
field = value
notifyPropertyChanged( BR.engineStrength )
}
init{
model = m
brand = b
year = y
engineStrength = es
}
}
Note a necessidade de marcar a entidade observável com a anotação @get:Bindable. Depois, seguindo a sintaxe Kotlin, temos de sobrescrever o método set() de cada propriedade que terá a característica de ser observável. Logo, aqui os campos têm de ser mutáveis, var.
field representa a propriedade do método set(), regra de sintaxe do Kotlin.
A classe BR é criada pela própria biblioteca Data Binding para conter a propriedade que deverá ser notificada quando houver um novo valor para ela.
Pode ser necessário um Build e Rebuild Project para que a classe BR surja já com as propriedades corretas.
O método notifyPropertyChanged() dispensa comentários, faz exatamente o que o rótulo dele indica: notifica uma mudança em propriedade.
Note que com este modelo de tipo observável, apesar de termos de colocar mais códigos, nós temos maior controle sobre quando notificar uma mudança.
Seguramente poderíamos colocar o método notifyPropertyChanged() dentro de condicionais de avaliação para definir quando ele deve ou não ser invocado.
Uma última observação. A propriedade brand não precisa ter nenhum tipo observável nela. Aqui vamos aproveitar a classe Brand já atualizada e então prosseguir assim. O trabalho com o bloco init{} também não é obrigatório, aqui o fiz para manter a igualdade de inicialização de objeto Car por meio do construtor, como fizemos nas outras seções.
Com isso podemos partir para um código de exemplo com a nossa nova versão de Car:
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
val binding = DataBindingUtil.setContentView<CarBinding>( this, R.layout.car )
val carBase = Car(
"Classe CLS",
Brand(
ObservableField( "Mercedes-Benz" ),
ObservableInt( 1871 )
),
2018,
4.9
)
binding.carBase = carBase
Thread{
kotlin.run {
/*
* Aplicando um delay para que seja possível
* visualizar a atualização da UI de acordo
* com a mudança de valor no objeto vinculado
* a ela.
* */
SystemClock.sleep( 3000 )
carBase.model = "Fusion"
carBase.year = 2017
carBase.engineStrength = 2.3
carBase.brand = Brand(
ObservableField( "Ford" ),
ObservableInt( 1903 )
)
}
}.start()
}
}
Então o layout car.xml atualizado:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="carBase"
type="thiengo.com.br.databindingtests.Car" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Modelo: %s", carBase.model) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Marca: %s (%d)", carBase.brand.name, carBase.brand.since) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Ano: %d", carBase.year) }' />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text='@{ String.format("Motor: %.1f", carBase.engineStrength) }' />
</LinearLayout>
</layout>
Executando o projeto com os novos códigos, temos:
Tipos observáveis em adaptadores de listas
É importante falar também dos tipos observáveis em adaptadores de lista, pois a principio o que pensamos é que as coleções observáveis nos permitem a fácil inclusão / remoção de itens e atualização de qualquer um deles.
Na verdade a atualização de qualquer um dos itens já em lista realmente vai ser refletida também em tela, assumindo que os objetos em lista têm suas propriedades observáveis.
Mas mesmo quando trabalhando com uma coleção observável, adicionar ou remover um item não tem efeito algum em tela.
Para que a adição (ou remoção) de um item tenha efeito visual, ainda é preciso também invocar algum dos métodos de notificação de atualização de lista: notifyDataSetChanged() ou notifyItemChanged().
Ou seja, ao menos para frameworks de lista, para termos efeito visual na adição, remoção ou atualização de posição de item, os códigos permanecem os mesmos quando não utilizando tipos observáveis.
Veja o hackcode a seguir:
/*
* cars é uma lista mutável de Car, ou seja,
* não é nem mesmo uma coleção observável.
* carNew é um novo objeto Car ainda não
* presente em tela.
* */
cars.add( carNew )
/*
* Fazendo com que a atualização de item
* também seja refletida em tela.
* */
recycler_view.adapter?.notifyDataSetChanged()
Pontos negativos
- Os tipos de coleções observáveis têm o exato mesmo efeito em classe adaptadora que qualquer outro tipo de coleção mutável. O processo de adição, remoção e atualização de posição de item ainda exige a invocação de algum método de notificação da classe adaptadora;
- A documentação não oferece exemplos implementando a Interface Observable.
Ponto positivo
- Permite que nós desenvolvedores Android não mais tenhamos de se preocupar também com os códigos de atualização de dados em tela.
Considerações finais
Que a biblioteca Data Binding é importante você certamente já deve estar ciente, principalmente em contexto de mercado de trabalho.
Os tipos observáveis desta biblioteca nos dão ainda mais arsenal de programação, também por remover de nosso tempo de desenvolvimento a necessidade de criar códigos de atualização de dados em tela.
Mas há um custo para isso: trabalhar os tipos da API ou implementar a classe BaseObservable, está última que exige ainda mais linhas de código e conhecimento Kotlin.
Projeto Android
Para o projeto de exemplo construiremos a área de contatos de um aplicativo Android de mensagens.
Nessa área terão alguns pontos que serão necessárias atualizações quando novas mensagens chegarem ao usuário do app, nesses pontos é que passaremos a utilizar os tipos observáveis da Data Binding API.
O projeto será desenvolvido em duas partes:
- Na primeira teremos todo o código sem uso dos tipos observáveis Data Binding;
- Na segunda vamos a evolução do projeto, utilizando tipos observáveis.
O projeto está presente no GitHub a seguir: https://github.com/viniciusthiengo/q-message.
Como indico em outros artigos e projetos aqui do Blog, mesmo com o aplicativo pronto no GitHub dele, recomendo que siga o passo a passo de sua construção e evolução para entender ainda mais as vantagens da API em estudo.
Protótipo estático
A seguir as imagens do protótipo estático do projeto de app. Este protótipo permanecerá assim mesmo na segunda parte, pois a atualização será apenas em código dinâmico:
Tela de entrada | Tela de listagem de contatos |
Iniciando o projeto
Em seu Android Studio inicie um novo projeto Kotlin:
- Nome da aplicação: QMessage;
- API mínima: 16 (Android Jelly Bean). Atendendo a mais de 99% dos aparelhos Android em mercado;
- Atividade inicial: Basic Activity;
- Nome da atividade inicial: ContactsActivity. 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:
Configurações Gradle
A seguir as configurações do Gradle Project Level, ou build.gradle (Project: QMessage):
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()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Então as configurações do Gradle App Level, ou build.gradle (Module: app):
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
/* Data Binding library. */
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
defaultConfig {
applicationId "thiengo.com.br.qmessage"
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'
}
}
/* Data Binding library. */
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-rc02'
implementation 'com.android.support:design:28.0.0-rc02'
/* Data Binding library. */
kapt "com.android.databinding:compiler:$android_plugin_version"
/* Para a RoundedImageView */
implementation 'com.makeramen:roundedimageview:2.3.0'
}
Ambas as configurações permanecerão as mesmas até o final do projeto. Sempre escolha por utilizar as versões estáveis mais recentes de cada API e SDK referenciados em Gradle.
Configurações AndroidManifest
Abaixo 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.qmessage">
<application
android:name=".CustomApplication"
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=".ContactsActivity"
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>
</application>
</manifest>
Como com os arquivos Gradle, este aqui também permanecerá sem atualizações.
Configurações de estilo
Todos os arquivos para configuração de estilo são simples, vamos iniciar pelo arquivo de cores, /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#9C27B0</color>
<color name="colorPrimaryDark">#7B1FA2</color>
<color name="colorPrimaryLight">#E1BEE7</color>
<color name="colorAccent">#FFC107</color>
<color name="colorNotification">#FF0000</color>
<color name="colorContactBackground">#F0F0F0</color>
<color name="colorDarkGrey">#666666</color>
<color name="colorGrey">#999999</color>
<color name="colorMediumGrey">#AAAAAA</color>
<color name="colorLightGrey">#BBBBBB</color>
<color name="colorLighterGrey">#CCCCCC</color>
</resources>
Então o simples arquivo de dimensões, /res/values/dimens.xml:
<resources>
<dimen name="item_margin">4dp</dimen>
</resources>
Assim o arquivo de Strings, /res/values/strings.xml:
<resources>
<string name="app_name">QMessage</string>
<string name="action_search_contact">Buscar contato</string>
<string name="last_answer_time_label">Última resposta:</string>
<string name="ic_desc_context_menu">Ícone de menu de contexto</string>
<string name="ic_desc_right_arrow">
Ícone de seta apontando para a última mensagem enviada
</string>
<string name="a_time">à</string>
<string name="year">ano</string>
<string name="years">anos</string>
<string name="month">mês</string>
<string name="months">meses</string>
<string name="week">semana</string>
<string name="weeks">semanas</string>
<string name="day">dia</string>
<string name="days">dias</string>
<string name="hour">hora</string>
<string name="hours">horas</string>
<string name="minute">minuto</string>
<string name="minutes">minutos</string>
<string name="second">segundo</string>
<string name="seconds">segundos</string>
</resources>
Por fim o arquivo de definição de estilo, tema, /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>
<!--
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" />
</resources>
Classes de domínio
Teremos duas classes de domínio no pacote /domain. Uma representando o contato do usuário e a outra representando a última mensagem de cada contato, mensagem enviada ao usuário.
Primeiro a classe LastMessage:
class LastMessage(
var time: Long,
var message: String
) : Parcelable {
fun lastMessageQuoted() =
String.format( "\"%s\"", message )
/*
* Método responsável por retornar o valor de
* lastTimeAnswer em um formato humano, pois está
* propriedade tem o valor em milissegundos.
* */
fun lastTimeAnswerFormatted( context: Context ): String {
var howLong: Int
val labelType: Int
val lastTime = Calendar.getInstance()
val timeNow = Calendar.getInstance()
val label: String
lastTime.timeInMillis = time
if( lastTime.get(Calendar.YEAR) != timeNow.get(Calendar.YEAR) ) {
howLong = timeNow.get(Calendar.YEAR) - lastTime.get(Calendar.YEAR)
labelType = Calendar.YEAR
}
else if( lastTime.get(Calendar.MONTH) != timeNow.get(Calendar.MONTH) ) {
howLong = timeNow.get(Calendar.MONTH) - lastTime.get(Calendar.MONTH)
labelType = Calendar.MONTH
}
else if( lastTime.get(Calendar.WEEK_OF_MONTH) != timeNow.get(Calendar.WEEK_OF_MONTH) ) {
howLong = timeNow.get(Calendar.WEEK_OF_MONTH) - lastTime.get(Calendar.WEEK_OF_MONTH)
labelType = Calendar.WEEK_OF_MONTH
}
else if( lastTime.get(Calendar.DAY_OF_MONTH) != timeNow.get(Calendar.DAY_OF_MONTH) ) {
howLong = timeNow.get(Calendar.DAY_OF_MONTH) - lastTime.get(Calendar.DAY_OF_MONTH)
labelType = Calendar.DAY_OF_MONTH
}
else if( lastTime.get(Calendar.HOUR_OF_DAY) != timeNow.get(Calendar.HOUR_OF_DAY) ) {
howLong = timeNow.get(Calendar.HOUR_OF_DAY) - lastTime.get(Calendar.HOUR_OF_DAY)
labelType = Calendar.HOUR_OF_DAY
}
else if( lastTime.get(Calendar.MINUTE) != timeNow.get(Calendar.MINUTE) ) {
howLong = timeNow.get(Calendar.MINUTE) - lastTime.get(Calendar.MINUTE)
labelType = Calendar.MINUTE
}
else {
howLong = timeNow.get(Calendar.SECOND) - lastTime.get(Calendar.SECOND)
howLong = if( howLong == 0 ) 1 else howLong
labelType = Calendar.SECOND
}
label = getLastTimeAnswerLabel( context, labelType, howLong )
return String
.format(
"%s %d %s",
context.getString(R.string.a_time),
howLong,
label
)
}
/*
* Método responsável por retornar o rótulo correto
* de acordo com o tempo já corrido desde a última
* mensagem.
* */
private fun getLastTimeAnswerLabel(
context: Context,
type: Int,
howLong: Int ): String {
val labelId = when( type ) {
Calendar.YEAR -> if( howLong > 1 ) R.string.years else R.string.years
Calendar.MONTH -> if( howLong > 1 ) R.string.months else R.string.month
Calendar.WEEK_OF_MONTH -> if( howLong > 1 ) R.string.weeks else R.string.week
Calendar.DAY_OF_MONTH -> if( howLong > 1 ) R.string.days else R.string.day
Calendar.HOUR_OF_DAY -> if( howLong > 1 ) R.string.hours else R.string.hour
Calendar.MINUTE -> if( howLong > 1 ) R.string.minutes else R.string.minute
else -> if( howLong > 1 ) R.string.seconds else R.string.second
}
return context.getString(labelId)
}
constructor( source: Parcel ) : this(
source.readLong(),
source.readString()
)
override fun describeContents() = 0
override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeLong( time )
writeString( message )
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<LastMessage> = object : Parcelable.Creator<LastMessage> {
override fun createFromParcel( source: Parcel ): LastMessage =
LastMessage( source )
override fun newArray( size: Int ): Array<LastMessage?> =
arrayOfNulls( size )
}
}
}
Agora a classe Contact:
class Contact(
val id: Int,
val image: Int,
val name: String,
var lastMessage: LastMessage,
var newMessages: Int
) : Parcelable {
constructor( source: Parcel ) : this(
source.readInt(),
source.readInt(),
source.readString(),
source.readParcelable<LastMessage>( LastMessage::class.java.classLoader ),
source.readInt()
)
override fun describeContents() = 0
override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeInt( id )
writeInt( image )
writeString( name )
writeParcelable( lastMessage, 0 )
writeInt( newMessages )
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<Contact> = object : Parcelable.Creator<Contact> {
override fun createFromParcel( source: Parcel ): Contact =
Contact( source )
override fun newArray( size: Int ): Array<Contact?> =
arrayOfNulls( size )
}
}
}
Ambas as classes implementando a Interface Parcelable, pois será preciso enviar objetos destes tipos por meio de uma Intent, isso quando chegarmos ao algoritmo de simulação de notificação push presente em uma Application customizada.
Observação: não deixe de ler todos os comentários encontrados nos códigos a partir desta seção.
Base de dados simulados
No pacote /data teremos um banco de dados simulados, mock data. Logo, neste pacote crie a classe Database como a seguir:
class Database {
companion object {
fun getContacts(): MutableList<Contact> {
val time = System.currentTimeMillis()
return mutableListOf(
Contact(
65,
R.drawable.person_01,
"Juliano Alves Cunha",
LastMessage(
time - (2 * 60 * 1000),
"Vc vai ao casamento?"
),
3
),
Contact(
98,
R.drawable.person_02,
"Rafaela Costa",
LastMessage(
time - (5 * 60 * 1000),
"Provavelmente é o nível 2, quase sempre"
),
1
),
Contact(
13,
R.drawable.person_03,
"Neiliane Almeida Ferreira",
LastMessage(
time - (8 * 60 * 1000),
"Somente dessa vez."
),
2
),
Contact(
2,
R.drawable.person_04,
"Ana barcellos",
LastMessage(
time - (9 * 60 * 1000),
"Certamente ele entrará em contato solic"
),
7
),
Contact(
9856,
R.drawable.person_05,
"Jordão Souza",
LastMessage(
time - (14 * 60 * 1000),
"Eu vou também."
),
0
),
Contact(
33658,
R.drawable.person_06,
"Gabriela Silveira",
LastMessage(
time - (19 * 60 * 1000),
"Tudo certo então."
),
0
)
)
}
fun getUserLogged() =
Contact(
69,
R.drawable.user_logged,
"Thiengo Android",
LastMessage(
0,
""
),
0
)
}
}
Os métodos são todos autocomentados, indicando o que cada um deles faz, respectivamente: retornar a lista de contatos; retornar o usuário conectado em app.
Classe adaptadora de contatos
Assim podemos partir para a camada de visualização. Começando pelo layout de item da classe adaptadora. Note que já estamos fazendo uso da Data Binding API, somente dos tipos observáveis que ainda não.
Segue XML de /res/layout/contact.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">
<data>
<import type="android.view.View" />
<variable
name="contact"
type="thiengo.com.br.qmessage.domain.Contact" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginEnd="@dimen/item_margin"
android:layout_marginLeft="@dimen/item_margin"
android:layout_marginRight="@dimen/item_margin"
android:layout_marginStart="@dimen/item_margin"
android:layout_marginTop="@dimen/item_margin"
android:background="@drawable/item_background"
android:padding="10dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".ContactsActivity"
tools:showIn="@layout/activity_contacts">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_profile"
android:layout_width="78dp"
android:layout_height="78dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:contentDescription="@{ contact.name }"
android:scaleType="centerCrop"
android:src='@{ String.format("android.resource://%s/%d", context.getPackageName(), contact.image) }'
app:riv_border_width="0dp"
app:riv_corner_radius="3dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/iv_profile"
android:layout_toRightOf="@+id/iv_profile"
android:ellipsize="end"
android:maxLines="1"
android:paddingEnd="28dp"
android:paddingRight="28dp"
android:text="@{ contact.name }"
android:textColor="@color/colorDarkGrey"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_last_answer_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_name"
android:layout_alignStart="@+id/tv_name"
android:layout_below="@+id/tv_name"
android:text="@string/last_answer_time_label"
android:textColor="@color/colorMediumGrey"
android:textSize="10sp"
android:textStyle="italic" />
<TextView
android:id="@+id/tv_last_answer_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_name"
android:layout_alignStart="@+id/tv_name"
android:layout_below="@+id/tv_last_answer_label"
android:text="@{ contact.lastMessage.lastTimeAnswerFormatted( context ) }"
android:textColor="@color/colorDarkGrey"
android:textSize="13sp" />
<ImageView
android:id="@+id/iv_last_answer"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignLeft="@+id/tv_name"
android:layout_alignStart="@+id/tv_name"
android:layout_below="@+id/tv_last_answer_time"
android:contentDescription="@string/ic_desc_right_arrow"
android:src="@drawable/ic_right_arrow"
android:tint="@color/colorLightGrey" />
<TextView
android:id="@+id/tv_last_answer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_last_answer"
android:layout_marginEnd="35dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="35dp"
android:layout_marginStart="1dp"
android:layout_toEndOf="@+id/iv_last_answer"
android:layout_toRightOf="@+id/iv_last_answer"
android:ellipsize="end"
android:maxLines="1"
android:text="@{ contact.lastMessage.lastMessageQuoted() }"
android:textColor="@color/colorGrey"
android:textSize="13sp"
android:textStyle="italic" />
<ImageView
android:id="@+id/iv_menu"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:contentDescription="@string/ic_desc_context_menu"
android:src="@drawable/ic_menu"
android:tint="@color/colorLighterGrey" />
<TextView
android:id="@+id/tv_new_messages"
android:layout_width="23dp"
android:layout_height="23dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:background="@drawable/ic_new_message"
android:gravity="center_horizontal"
android:text="@{ String.valueOf( contact.newMessages ) }"
android:textColor="@android:color/white"
android:textSize="13sp"
android:textStyle="bold"
android:visibility="@{ contact.newMessages > 0 ? View.VISIBLE : View.GONE }" />
</RelativeLayout>
</layout>
A seguir o arquivo drawable que serve de background para cada item de lista, arquivo referenciado logo no ViewGroup principal, RelativeLayout. Segue /res/drawable/item_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 do componente visual.
-->
<solid android:color="@color/colorContactBackground" />
<!--
Curvatura de borda do componente visual.
-->
<corners android:radius="5dp" />
</shape>
Com o arquivo drawable anterior conseguimos o seguinte resultado em tela, para cada item:
Assim o diagrama do layout contact.xml:
Por fim o simples código Kotlin da classe ContactsAdapter:
class ContactsAdapter(
private val context: Context,
private val contacts: List<Contact> ) :
RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int ) : ContactsAdapter.ViewHolder {
val inflater = LayoutInflater.from( context )
val binding = ContactBinding
.inflate( inflater, parent, false )
return ViewHolder( binding )
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int ) {
holder.setData( contacts[ position ] )
}
override fun getItemCount(): Int {
return contacts.size
}
inner class ViewHolder( val binding: ContactBinding) :
RecyclerView.ViewHolder( binding.root ) {
fun setData( contact: Contact ) {
binding.contact = contact
binding.executePendingBindings()
}
}
}
Atividade principal, listagem de contatos
Para a atividade principal vamos iniciar pela apresentação do layout, que por sinal também é simples.
Segue /res/layout/top_bar.xml layout que contém a configuração personalizada do Toolbar, configuração com a imagem de perfil do usuário junto ao nome dele:
<?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>
<variable
name="user"
type="thiengo.com.br.qmessage.domain.Contact" />
</data>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
app:title="@null"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_profile"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="18dp"
android:layout_marginRight="18dp"
android:scaleType="fitCenter"
android:src='@{ String.format("android.resource://%s/%d", context.getPackageName(), user.image) }'
app:riv_border_width="0dp"
app:riv_corner_radius="3dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ user.name }"
android:textColor="@android:color/white"
android:textSize="21sp" />
</android.support.v7.widget.Toolbar>
</layout>
A seguir o diagrama do layout anterior:
Agora o XML do layout principal da atividade de contatos, /res/layout/activity_contacts.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="@android:color/white"
tools:context=".ContactsActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<include layout="@layout/top_bar" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_contacts"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
Abaixo o diagrama do layout anterior:
E assim o código Kotlin de ContactsActivity:
class ContactsActivity : AppCompatActivity() {
lateinit var contacts: MutableList<Contact>
lateinit var broadcast: BroadcastNotification
lateinit var thread: Thread
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_contacts )
setSupportActionBar( toolbar )
/*
* Removendo o título padrão do app da barra
* de topo dele.
* */
supportActionBar?.setDisplayShowTitleEnabled( false )
initTopBar()
contacts = Database.getContacts()
initContactsList()
initBroadcastReceiver()
}
private fun initTopBar(){
val binding = TopBarBinding.bind( toolbar )
binding.user = Database.getUserLogged()
}
private fun initContactsList(){
rv_contacts.setHasFixedSize( true )
val layoutManager = LinearLayoutManager( this )
rv_contacts.layoutManager = layoutManager
rv_contacts.adapter = ContactsAdapter(
this,
contacts
)
runAdapterTime()
}
override fun onCreateOptionsMenu( menu: Menu ): Boolean {
menuInflater.inflate( R.menu.menu_contacts, menu )
return true
}
/*
* Método responsável por registrar um BroadcastReceiver
* (BroadcastNotification) para poder receber uma comunicação
* de CustomApplication, comunicação sobre uma nova mensagem,
* notificação, que chegou do servidor Web (simulação).
* */
private fun initBroadcastReceiver(){
broadcast = BroadcastNotification( this )
val filter = IntentFilter( BroadcastNotification.FILTER )
LocalBroadcastManager
.getInstance( this )
.registerReceiver( broadcast, filter )
}
override fun onDestroy() {
super.onDestroy()
/*
* Liberação do BroadcastReceiver.
* */
LocalBroadcastManager
.getInstance( this )
.unregisterReceiver( broadcast )
/*
* Desligando Thread de timer.
* */
thread.interrupt()
}
/*
* Método responsável por atualizar o tempo das últimas
* mensagens enviadas para cada contato em lista.
* */
private fun runAdapterTime(){
thread = Thread{
kotlin.run {
while( true ){
/*
* Delay de 6 segundos, mas é seguro colocar
* o mínimo de 1 minuto para evitar vazamnto
* de memória.
* */
SystemClock.sleep( 60 * 100 )
/*
* Como a atualização será refletida em
* tela é importante que ela ocorra na
* Thread principal, UI.
* */
runOnUiThread {
rv_contacts?.adapter?.notifyDataSetChanged()
}
}
}
}
thread.start()
}
/*
* Método responsável por atualizar a lista de contatos
* assim que uma nova mensagem chega via notificação push.
* */
fun updateContactsList( contact: Contact ){
val item = contacts.find {
it.id == contact.id
}
item!!.newMessages = contact.newMessages
item.lastMessage = contact.lastMessage
contacts.remove( item )
contacts.add( 0, item )
rv_contacts.adapter?.notifyDataSetChanged()
}
}
LocalBroadcast para transporte de mensagem push
Aqui trabalharemos com um LocalBroadcastManager para transporte de notificação push da Application para a ContactsActivity.
Na raiz do projeto crie a classe BroadcastNotification como a seguir:
/*
* LocalBroadcast que permitirá a fácil comunicação entre
* CustomApplication e ContactsActivity.
* */
class BroadcastNotification(
val activity: ContactsActivity ): BroadcastReceiver() {
companion object {
const val FILTER = "bn_filter"
const val DATA = "bn_data"
}
override fun onReceive( context: Context?, intent: Intent? ) {
/*
* Como sabemos que getParcelableExtra() nunca retornará
* null para este trecho do projeto, seguramente podemos
* trabalhar o operador force, !!, para que o IDE permita
* a continuidade na compilação.
* */
val contact = intent!!.getParcelableExtra<Contact>( DATA )
activity.updateContactsList( contact )
}
}
Application para simulação de notificação push
Para simular a chegada de uma notificação push no aplicativo, vamos utilizar uma Application customizada. Ainda na raiz do projeto crie a classe CustomApplication como a seguir:
class CustomApplication: Application() {
override fun onCreate() {
super.onCreate()
/*
* Algoritmo responsável por simular a chegada de uma
* notificação onde os dados, um objeto Contact, serão
* enviados a atividade principal para processamento e
* reflexo em tela.
* */
Thread{
kotlin.run {
/*
* Delay de 3 segundos para simular o delay de
* rede para a chegada de notificação push.
* */
SystemClock.sleep( 3000 )
/*
* É importante que o objeto em notificação tenha
* o id do contato, a nova mensagem e o número de
* novas mensagens não lidas.
* */
val contact = Contact(
9856,
0,
"",
LastMessage(
System.currentTimeMillis(),
"Eu vou também."
),
2
)
val intent = Intent( BroadcastNotification.FILTER )
intent.putExtra( BroadcastNotification.DATA, contact )
LocalBroadcastManager
.getInstance( this )
.sendBroadcast( intent )
}
}.start()
}
}
Note que essa CustomApplication já está sendo referenciada no AndroidManifest.xml:
...
<application
android:name=".CustomApplication"
...
Para melhor entender o funcionamento do algoritmo de simulação de notificação push, veja o fluxograma a seguir:
Atualização de projeto, colocando tipos observáveis
Mesmo com o projeto de exemplo sendo simples e pequeno, com a utilização de tipos observáveis ainda teremos o ganho de não mais termos de nos preocuparmos com os algoritmos de atualização de dados em tela, isso ocorrerá de maneira automática.
Tendo em mente que se o projeto estivesse construído por completo, incluindo conexão com um banco de dados remoto, o ganho em código seria ainda mais evidente.
Sendo assim, vamos a atualização do app Android QMessage.
Atualizando a classe de domínio LastMessage
Nosso primeiro passo é atualizar todas as classes de domínio, pois em nosso projeto todas as propriedades dessas classes estão sendo referenciadas em layout com a sintaxe Data Binding.
Iniciando com a atualização da menor delas, LastMessage:
class LastMessage(
var time: ObservableLong,
var message: ObservableField<String> ) : Parcelable {
fun lastMessageQuoted() =
String.format( "\"%s\"", message.get() )
/*
* Método responsável por retornar o valor de
* lastTimeAnswer em um formato humano, pois está
* propriedade tem o valor em milissegundos.
* */
fun lastTimeAnswerFormatted( context: Context ): String {
var howLong: Int
val labelType: Int
val lastTime = Calendar.getInstance()
val timeNow = Calendar.getInstance()
val label: String
lastTime.timeInMillis = time.get()
...
}
...
constructor( source: Parcel ) : this(
source.readParcelable<ObservableLong>( ObservableLong::class.java.classLoader ),
source.readSerializable() as ObservableField<String>
)
...
override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeParcelable( time, 0 )
writeSerializable( message )
}
...
}
No código acima colocamos, em destaque, somente os trechos que passaram por atualizações. O restante da classe continua como já estava.
Atualizando a classe de domínio Contact
Agora a classe Contact com os tipos observáveis:
class Contact(
val id: ObservableInt,
val image: ObservableInt,
val name: ObservableField<String>,
var lastMessage: LastMessage,
var newMessages: ObservableInt
) : Parcelable {
constructor( source: Parcel ) : this(
source.readParcelable<ObservableInt>(ObservableInt::class.java.classLoader),
source.readParcelable<ObservableInt>(ObservableInt::class.java.classLoader),
source.readSerializable() as ObservableField<String>,
source.readParcelable<LastMessage>(LastMessage::class.java.classLoader),
source.readParcelable<ObservableInt>(ObservableInt::class.java.classLoader)
)
...
override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeParcelable( id, 0 )
writeParcelable( image, 0 )
writeSerializable( name )
writeParcelable( lastMessage, 0 )
writeParcelable( newMessages, 0 )
}
...
}
Note que como Contact implementa Parcelable e LastMessage também, não devemos utilizar o tipo observável ObservableParcelable, pois assim nem mesmo compilar o projeto conseguiremos. LastMessage já implementa o Parcelable e com isso essa classe dispensa o uso de ObservableParcelable em Contact.
Agora temos de atualizar os trechos de código que criam objetos Contact e LastMessage.
Atualizando a base de dados mock
O novo código da classe Database fica como a seguir:
class Database {
companion object {
fun getContacts(): MutableList<Contact> {
val time = System.currentTimeMillis()
return mutableListOf(
Contact(
ObservableInt(65),
ObservableInt(R.drawable.person_01),
ObservableField("Juliano Alves Cunha"),
LastMessage(
ObservableLong(time - (2 * 60 * 1000)),
ObservableField("Vc vai ao casamento?")
),
ObservableInt(3)
),
Contact(
ObservableInt(98),
ObservableInt(R.drawable.person_02),
ObservableField("Rafaela Costa"),
LastMessage(
ObservableLong(time - (5 * 60 * 1000)),
ObservableField("Provavelmente é o nível 2, quase sempre")
),
ObservableInt(1)
),
Contact(
ObservableInt(13),
ObservableInt(R.drawable.person_03),
ObservableField("Neiliane Almeida Ferreira"),
LastMessage(
ObservableLong(time - (8 * 60 * 1000)),
ObservableField("Somente dessa vez.")
),
ObservableInt(2)
),
Contact(
ObservableInt(2),
ObservableInt(R.drawable.person_04),
ObservableField("Ana barcellos"),
LastMessage(
ObservableLong(time - (9 * 60 * 1000)),
ObservableField("Certamente ele entrará em contato solic")
),
ObservableInt(7)
),
Contact(
ObservableInt(9856),
ObservableInt(R.drawable.person_05),
ObservableField("Jordão Souza"),
LastMessage(
ObservableLong(time - (14 * 60 * 1000)),
ObservableField("Eu vou também.")
),
ObservableInt(0)
),
Contact(
ObservableInt(33658),
ObservableInt(R.drawable.person_06),
ObservableField("Gabriela Silveira"),
LastMessage(
ObservableLong(time - (19 * 60 * 1000)),
ObservableField("Tudo certo então.")
),
ObservableInt(0)
)
)
}
fun getUserLogged() =
Contact(
ObservableInt(69),
ObservableInt(R.drawable.user_logged),
ObservableField("Thiengo Android"),
LastMessage(
ObservableLong(0),
ObservableField("")
),
ObservableInt(0)
)
}
}
Atualizando a CustomApplication
Para a CustomApplication atualize o código como a seguir:
...
override fun onCreate() {
...
Thread{
kotlin.run {
...
/*
* É importante que o objeto em notificação tenha
* o id do contato, a nova mensagem e o número de
* novas mensagens não lidas.
* */
val contact = Contact(
ObservableInt( 9856 ),
ObservableInt( 0 ),
ObservableField( "" ),
LastMessage(
ObservableLong( System.currentTimeMillis() ),
ObservableField( "Eu vou também." )
),
ObservableInt( 2 )
)
...
}
}.start()
}
...
Atualizando o método updateContactsList()
Ainda temos de atualizar o método updateContactsList() da ContactsActivity:
...
fun updateContactsList( contact: Contact ){
val item = contacts.find {
it.id.get() == contact.id.get()
}
item!!.newMessages.set( contact.newMessages.get() )
item.lastMessage.time.set( contact.lastMessage.time.get() )
item.lastMessage.message.set( contact.lastMessage.message.get() )
contacts.remove( item )
contacts.add( 0, item )
rv_contacts.adapter?.notifyDataSetChanged()
}
...
Toda a configuração de layout permanece a mesma. Assim podemos partir para os testes.
Testes e resultados
Acesse o menu de topo do Android Studio. Acione Build e em seguida acione Rebuild project. Agora execute o aplicativo em seu emulador ou aparelho de testes.
Aguardando alguns segundos, temos:
Com isso terminamos mais um importante conteúdo sobre a biblioteca Data Binding, os tipos observáveis dela.
Não deixe de se inscrever na 📩 lista de e-mails 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 dos tipos observáveis da biblioteca Data Binding:
Vídeos
A seguir os vídeos com o passo a passo da implementação de tipos observáveis Data Binding no projeto Android QMessage:
Para acessar o projeto de exemplo, entre no GitHub dele em: https://github.com/viniciusthiengo/q-message.
Conclusão
Os tipos observáveis têm como principal objetivo remover do desenvolvedor a necessidade de também trabalhar os algoritmos de atualização de dados em tela.
Algo muito similar ao que faz o banco de dados Realm, que na atualização de propriedades vinculadas a base de dados não há necessidade de algum commit, a atualização já é refletida na persistência.
Trabalhando com Data Binding em seus projetos de aplicativos Android não deixe de também utilizar os tipos observáveis, para cada vez mais remover as preocupações de atualização de UI em código dinâmico.
Assim finalizamos o conteúdo. 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 e-mails.
Abraço.
Fontes
Work with observable data objects
Documentação oficial ObservableParcelable
ObservableParcelable<String> is not working - Resposta de Виталий Махнев
adding profile pic,username on toolbar - Resposta de Sanjeet A e Community♦
Toolbar image centered - Resposta de Abhinav Puri
How to remove app title from toolbar? - Resposta de younes
Comentários Facebook