Como Reter Objetos Utilizando Android-State API

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

Email inválido.
Blog /Android /Como Reter Objetos Utilizando Android-State API

Como Reter Objetos Utilizando Android-State API

Vinícius Thiengo
(4241)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloCraftsmanship Limpo: Disciplinas, Padrões e ética
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição
Ano2023
Páginas416
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Tudo bem?

Neste artigo vamos, passo a passo, trabalhar com a API Android-State para, de maneira simples, aprender a reter estados de objetos contidos em atividadesfragmentos e visualizações.

Primeiro vamos a apresentação da API, do porquê de os desenvolvedores do Evernote terem construído está, e  posteriormente vamos a integração da library em um projeto de Copa do Mundo de Futebol.

Animação aplicativo com Android-State em uso

Antes de prosseguir, não esqueça de se inscrever 📩 na lista de e-mails do Blog para receber os conteúdos exclusivos e em primeira mão.

Caso você queira algo rápido, somente o conteúdo sobre a biblioteca em estudo, sem uso de projeto de exemplo, ao final do artigo tem a versão dele em slides. Caso prefira o conteúdo em vídeo, logo depois dos slides está o vídeo.

Abaixo os tópicos que estaremos abordando:

Retenção de objetos em aplicativos Android

A retenção de estados de objetos em aplicativos Android é algo já trabalhado desde as primeiras versões desta plataforma.

As estratégias que se destacam são: utilizando o ViewModel ou utilizando o SaveInstanceState. Mas também é possível trabalhar bases de dados locais para isso, como, por exemplo: SharedPreferencesSQLite ou Realm.

Não há uma solução superior a todas as outras, há a que se encaixa melhor para um problema em específico. O importante é permitir que objetos trabalhados em atividades e fragmentos, ao menos esses, não sejam criados novamente devido a reconstrução de algum destes componentes Android.

A reconstrução de objetos não deve ser uma preocupação somente caso não tenha o mínimo efeito na performance do aplicativo.

Evernote Android-State

O time de desenvolvimento do Evernote aprimorou a API Icepick, API já famosa por conseguir, de maneira trivial, salvar o estado de objetos de atividades, fragmentos e visualizações. Com isso surgiu a Android-State library.

A Android-State é mais robusta, funciona tanto para projetos Java como para projetos Kotlin. Além de não exigir que as propriedades sejam públicas, algo que uma API não deve fazer: forçar desenvolvedores a seguirem o caminho delas de arquitetura de software.

Como o Icepick o Android-State também é de código aberto. Importante ter em mente que o que o Android-State faz é exatamente o que o Icepick faz, mas abrangendo mais ramos, atendendo o que era limitação no Icepick.

Ok, mas o que realmente essas APIs fazem?

Elas trabalham o SaveInstanceState para nós developers, evitando que tenhamos de adicionar código boilerplate ao projeto.

Então ainda temos presente nessas bibliotecas as limitações do SaveInstanceState?

Sim. A mesma tabela apresentada no conteúdo do ViewModel ainda é válida aqui:

 Android-State / IcepickViewModel
Durabilidade dos dados em memóriaMaiorMenor
Quantidade de dados (bytes) em memóriaMenorMaior
 Auxílio de Interface para serialização e desserialização de objetosSimNão

Lembrando que a tabela acima somente lhe ajuda a escolher qual a melhor API para o seu domínio do problema.

Com isso podemos partir para as configurações e codificação da Android-State API

Instalação da API

No Gradle App Level, ou build.gradle (Module: app), de seu projeto Android, adicione as seguintes referências:

...
dependencies {
...

implementation 'com.evernote:android-state:1.2.1'

/* Para projeto somente em Java */
annotationProcessor 'com.evernote:android-state-processor:1.2.1'

/* Para projeto Kotlin com ou sem Java */
kapt 'com.evernote:android-state-processor:1.2.1'
}

 

Logo depois sincronize o projeto. Pode ser que você tenha problemas de conflito de API caso não esteja com a versão mais atual do Android Support APIs, logo, caso ocorra, utilize como estratégia de correção de conflito: atualização do Android Support APIs para a versão mais atual.

Na época da construção deste conteúdo a versão mais atual era a 27.1.0.

Utilizando em todo o projeto, via Application class customizada

Para que seja possível manter o estado de instâncias em atividades e fragmentos de todo o projeto, primeiro temos de criar uma classe Application customizada com o StateSaver definido:

class App: Application() {
override fun onCreate() {
super.onCreate()
StateSaver.setEnabledForAllActivitiesAndSupportFragments(this, true)
}
}

 

Depois devemos referencia-la no AndroidManifest.xml:

...
<application
android:name=".App"
...>
...

 

E assim utilizar a anotação @State nas entidades que queremos manter o estado:

class MainActivity : AppCompatActivity() {

@State
var numbers: ArrayList<Int> = arrayListOf()

@State
var number = 28
...
}

 

Diferente da Icepick API, com Android-State as propriedades podem ser públicas e privadas.

No código acima a inicialização das propriedades não mais será levada em conta caso haja uma reconstrução de atividade, somente os algoritmos dentro, direta ou indiretamente, dos métodos de ciclo de vida da atividade.

A anotação funciona exatamente com a mesma sintaxe quando com fragmentos:

class MainFragment : Fragment() {

@State
var numbers: ArrayList<Int> = arrayListOf()

@State
var number = 28
...
}

 

Importante: somente tipos de dados aceitos em objetos Bundle é que são válidos de uso com o @State. São eles: String, todos os primitivos e objetos de tipos que implementam Parcelable ou Serializable.

Utilizando somente em componentes necessários

Quando a necessidade de retenção de estado é somente em alguns componentes do projeto, você pode ignorar a definição utilizando uma classe Application customizada e assim utilizar os métodos restoreInstanceState()saveInstanceState() do StateSaver:

class MainActivity : AppCompatActivity() {

@State
var numbers: ArrayList<Int> = arrayListOf()

@State
var number = 28

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
StateSaver.restoreInstanceState( this, savedInstanceState )
...
}

override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
StateSaver.saveInstanceState( this, outState!! )
}
}

 

Note que o outState!! tem as duas exclamações ao final para que o code inspector não acuse erro e assim trave a compilação, isso, pois StateSaver.saveInstanceState() está esperando um dado não null, porém para não modificar a assinatura nativa de onSaveInstanceState() optei por gerar uma exceção caso o outState seja nulo (algo improvável aqui).

Para fragmentos a sintaxe é a mesma:

class MainFragment : Fragment() {

@State
var numbers: ArrayList<Int> = arrayListOf()

@State
var number = 28

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
StateSaver.restoreInstanceState( this, savedInstanceState )
...
}

override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
StateSaver.saveInstanceState( this, outState!! )
}
}

Utilizando com visualização

Para manter o estado de propriedades em visualizações é preciso o trabalho explícito na View com a classe StateSaver, como a seguir:

class TestView(context: Context) : View(context) {

@State
var number: 28

protected fun onSaveInstanceState(): Parcelable {
return StateSaver.saveInstanceState( this, super.onSaveInstanceState() )
}

protected fun onRestoreInstanceState(state: Parcelable) {
super.onRestoreInstanceState( StateSaver.restoreInstanceState(this, state) )
}
}

 

Seguindo as indicações da documentação fica entendido que a definição global de uso do Android-State, com uma classe Application, não funciona para estados de propriedades em visualizações customizadas.

Visualizações customizadas?

Sim, pois por padrão, ao menos para as Views de entrada de dados, o sistema Android já mantém em tela o que foi informado pelo usuário, isso caso haja reconstrução de atividade ou fragmento.

Envelopando objetos de tipos que não são aceitos em Bundle

Caso você entre em um domínio de problema onde será necessário manter o estado de objetos de tipos que não implementam nem Parcelable e nem Serializable, para isso você terá de criar uma classe envelopadora que implemente Bundler<T>.

Esse tipo de situação é comum quando a tipo de objeto é de alguma das APIs de terceiros vinculadas ao seu projeto. Veja a classe a seguir:

class Game(
val group: String,
val countryA: Country,
val countryB: Country,
val dateTime: Date,
val stadium: String )

 

Assuma que os tipos Country e Date implementam ou o Parcelable ou o Serializable. Mesmo assim para mantermos algum objeto do tipo Game teremos de criar uma classe envelopadora (assuma que não podemos implementar em Game as interfaces comentadas anteriormente).

Podemos criar a classe GamerBundler como classe envelopadora que implementa Bundler<T>:

class GameBundler: Bundler<Game> {

override fun put(key: String, value: Game, bundle: Bundle) {
bundle.putString("$key-01", value.group)
bundle.putParcelable("$key-02", value.countryA)
bundle.putParcelable("$key-03", value.countryB)
bundle.putSerializable("$key-04", value.dateTime)
bundle.putString("$key-05", value.stadium)
}

override fun get(key: String, bundle: Bundle): Game? {
val game = Game(
bundle.getString("$key-01"),
bundle.getParcelable("$key-02") as Country,
bundle.getParcelable("$key-03") as Country,
bundle.getSerializable("$key-04") as Date,
bundle.getString("$key-05")
)
return game
}
}

 

Agora, em uma atividade de exemplo, temos:

class MainActivity : AppCompatActivity() {

@State(GameBundler::class)
var game = Game(
"H",
Country("Colômbia", R.drawable.colombia),
Country("Japão", R.drawable.japao),
Date( Database.inMilliseconds(19,9,0) ),
"Arena Mordovia"
)
...
}

 

Note que caso você esteja em um domínio do problema onde seja necessária a criação de mais de uma classe Bundler<T>, onde uma conterá a outra, para isso recomendo que utilize o ViewModel como solução para retenção de estados de objetos dos componentes Activity e Fragment.

Isso, pois você terá de criar uma outra rota para conseguir essa meta, por exemplo: criando uma classe para conter todos os dados das classes que não implementem as interfaces Parcelable e Serializable. Sendo assim a simplicidade adquirida com a Android-State API é perdida favorecendo então a escolha pelo ViewModel.

Caso você precise trabalhar com o tipo List<T>, vai notar que o compilador não permitirá prosseguir, pois List<T> não tem em sua hierarquia o trabalho com Parcelable ou Serializable. Para isso a biblioteca Android-State já tem como nativas as seguintes classes envelopadoras:

  • BundlerListParcelable;
  • BundlerListCharSequence;
  • BundlerListString;
  • BundlerListInteger.

Exemplo:

class MainActivity : AppCompatActivity() {

@State(BundlerListString::class)
var listString: List<String> = listOf()
...
}

Trabalhando com reflexão

Se por algum motivo as propriedades que devem ser mantidas são privadas e você não pode fornecer os métodos getter e setter delas, terá de utilizar a anotação @StateReflection para mante-las, trabalho com reflexão, algo recomendado pela API somente quando não há outra alternativa:

class MainActivity : AppCompatActivity() {

@StateReflection
private var number = 28
...
}

Proguard

Caso esteja com o Proguard ativo, terá de colocar as seguintes definições em proguard-rules.pro:

-dontwarn com.evernote.android.state.**
-keep class com.evernote.android.state.** { *; }
-keep class **$$StateSaver { *; }

# generated class names
-keepclasseswithmembernames class * { @com.evernote.android.state.State *;}
-keepclasseswithmembernames class * { @com.evernote.android.state.StateReflection *;}

# only keep non-private fields, there must be a getter / setter for private fields
-keepclassmembers class * {
@com.evernote.android.state.State !private <fields>;
}

# with reflection always keep the name of the field
-keepclassmembers class * { @com.evernote.android.state.StateReflection <fields>;}

Pontos negativos

  • A API não permite o trabalho com classes Bundler<T> internas a outras classes que implementem Bundler<T>;
  • Para realmente entender a API Android-State é preciso ler também a documentação da API Icepick;
  • Na documentação não fala nada sobre as limitações da API devido ao uso interno do SaveInstanceState, algo que um developer precisa saber para não cometer o erro de tentar salvar listas com centenas de objetos pesados em memória.

Pontos positivos

  • Muito mais simples de utilizar do que qualquer uma das outras soluções Android para retenção de estado de objetos de atividades, fragmentos e visualizações;
  • Funciona para todas as versões Android ainda em mercado;
  • Quando necessária, a criação de classe Bundler<T> ainda mantém o uso da API sendo simples;
  • Tem definições para o Lint Tool que facilitam o uso da API no Android Studio.

Considerações finais

A Android-State API é simples, isso induz ao seu uso, mas há casos onde é prudente realizar testes de carga, isso, pois é a API SaveInstanceState que está sendo utilizada "por debaixo dos panos" e sabemos da limitação de espaço em memória para está API.

Para projetos onde os dados são pouco ou nada dinâmicos e mesmo assim não devem passar pelas reconstruções de atividades, fragmentos e visualizações, seguramente recomendo Android-State.

Para outras situações: ficaria com o ViewModel. Também por este fazer parte da arquitetura recomendada pelo Google Android.

Projeto Android

Para uma apresentação real da API Android-State, vamos a construção de um aplicativo da Copa do Mundo 2018 de Futebol onde serão apresentados os confrontos da primeira rodada da fase de grupos.

Nossa problemática é que o aplicativo trabalha com uma lista de dados pouco dinâmica, porém sem nenhum recurso simples para rete-la no aplicativo caso haja reconstrução da atividade principal.

O projeto também pode ser encontrado, já finalizado, no seguinte GitHub: https://github.com/viniciusthiengo/fifa-world-cup-2018.

Mesmo com o projeto descarregado do GitHub, não deixe de acompanhar aqui a construção dele. Primeiro vamos ao desenvolvimento do projeto sem uso da API Android-State e depois, com poucas atualizações, vamos colocar a API para rodar no app.

Protótipo estático

A seguir as imagens do protótipo estático, desenvolvido antes mesmo de iniciar um novo projeto no Android Studio:

 Tela de entrada

 Tela de entrada

 Tela de listagem de jogos

 Tela de listagem de jogos

Assim podemos partir para a criação e codificação do projeto Android.

Iniciando o projeto

Com o Android Studio aberto, inicie um novo projeto Android Kotlin (pode ser Java caso prefira esta linguagem JVM). Como nome do projeto coloque "FIFA World Cup 2018". Como API mínima escolha a 16, Jelly Bean.

A atividade inicial será uma "Empty Activity" com o nome sugerido: MainActivity. Ao final desta primeira parte teremos a seguinte arquitetura de projeto:

Arquitetura Android Studio do projeto FIFA

Configurações Gradle

A seguir a configuração do Gradle Project Level, ou build.gradle (Project: FIFAWorldCup2018):

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

allprojects {
repositories {
google()
jcenter()
}
}

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

 

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

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

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

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.0'
implementation 'com.android.support:recyclerview-v7:27.1.0'
}

 

A versão acima do Gradle é a que passará por atualizações na segunda parte do projeto, isso para incluir a library Android-State.

Configurações AndroidManifest

As configurações do AndroidManifest.xml são simples:

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

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

<activity android:name=".MainActivity">
<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 configurações de estilo do aplicativo, vamos iniciar com as definições de cores em uso, isso no arquivo /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#c62828</color>
<color name="colorPrimaryDark">#8e0000</color>
<color name="colorAccent">#3f51b5</color>
<color name="colorItemOdd">#a53333</color>
<color name="colorItemEven">#d15353</color>
</resources>

 

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

<resources>
<string name="app_name">FIFA World Cup 2018</string>
<string name="vs">VS</string>
</resources>

 

Então o arquivo de definição de tema, /res/values/styles.xml:

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

Classes de domínio do problema

Temos duas classes de domínio e um arquivo para funções de extensão em Kotlin. Todos no pacote /domain. Vamos iniciar com a menor das classes, a que representa cada país no aplicativo, Country:

class Country(
var name: String,
val flagResource: Int)

 

Agora o arquivo de funções de extensão que contém novas funções para a classe Date do java.util. O rótulo do arquivo é extension_functions:

/*
* Conjunto de métodos de extensão para o tipo Date para
* facilitar o algoritmo principal que também precisa
* imprimir, formatado para humanos, a data e horário dos
* jogos da primeira rodada da copa do mundo.
* */
fun Date.getMonthLabel() : String{
return getHumanLabel( Calendar.MONTH )
}

fun Date.getDayOfMonthLabel() : String{
return getHumanLabel( Calendar.DAY_OF_MONTH )
}

fun Date.getHourOfDayLabel() : String{
return getHumanLabel( Calendar.HOUR_OF_DAY )
}

fun Date.getMinuteLabel() : String{
return getHumanLabel( Calendar.MINUTE )
}

private fun Date.getHumanLabel( type: Int ) : String{
val calendar = Calendar.getInstance()
calendar.timeInMillis = this.time

val number = calendar.get(type)
return if( number < 10 )
"0$number"
else
number.toString()
}

 

Primeiro: é importante que você leia todos os comentários presentes nos códigos apresentados aqui, pois eles complementam toda a explicação.

Segundo: sim, isso é possível no Kotlin, extender classes que já são definidas em pacotes que não são de nossa autoria. Alias essa é uma das vantagens das linguagens modernas, dessa forma conseguimos ter um código ainda mais fácil de interpretar, sem necessidade de classes utilitárias atendendo a mais de uma responsabilidade.

Para criar um arquivo que tenha funções de extensão sem ter a definição de uma classe, basta:

  • Clicar com o botão direito do mouse no pacote em que deseja que este arquivo esteja presente;
  • Logo depois acessar "New";
  • Então clicar em "Kotlin File/Class";
  • Na janela "New Kotlin File/Class" coloque o nome do arquivo e deixe selecionada a opção "File";
  • Clique em "Ok".

Animação para criação de arquivo Kotlin no Android Studio

A sintaxe é como no caso anterior para a classe Date, antes do nome da função coloque o nome da classe e um ponto de separação: fun NomeClasse.nomeFuncao(...) ...{}

Então podemos partir para a classe que representa os confrontos na Copa do Mundo, Game:

class Game(
val group: String,
val countryA: Country,
val countryB: Country,
val dateTime: Date,
val stadium: String) {

fun generateGroupLabel() = "Grupo $group"

fun generateDateTimeLabel(): String {
var gameTime = "${dateTime.getDayOfMonthLabel()}/"
gameTime += dateTime.getMonthLabel()
gameTime += " às ${dateTime.getHourOfDayLabel()}:"
gameTime += dateTime.getMinuteLabel()

return gameTime
}
}

 

Devido as funções de extensão o algoritmo de generateDateTimeLabel() ficou ainda mais simples de entender.

Note que todas as entidades específicas do domínio do problema estão também em inglês para não haver uma mistura de inglês e português no código fonte, tendo em mente que muitas APIs nativas e de terceiros, todas em inglês, estão sendo utilizadas.

Base de dados simulados, mock data

Para facilitar o trabalho com a API Android-State, vamos utilizar uma base de dados simulados, mock, para que o exemplo permaneça simples.

Crie um novo pacote nomeado /data e então adicione a classe Database:

class Database {
companion object {

/*
* Método responsável por simular uma base de dados mock.
* */
fun getGames() =
arrayListOf<Game>(
Game(
"A",
Country("Rússia", R.drawable.russia),
Country("Árabia Saudita", R.drawable.arabia_saudita),
Date( inMilliseconds(14,12,0) ),
"Luzhniki"
),
Game(
"A",
Country("Egito", R.drawable.egito),
Country("Uruguai", R.drawable.uruguai),
Date( inMilliseconds(15,9,0) ),
"Central"
),
Game(
"B",
Country("Marrocos", R.drawable.marrocos),
Country("Irã", R.drawable.ira),
Date( inMilliseconds(15,12,0) ),
"Krestovsky"
),
Game(
"B",
Country("Espanha", R.drawable.espanha),
Country("Portugal", R.drawable.portugal),
Date( inMilliseconds(15,15,0) ),
"Olímpico de Fisht"
),
Game(
"C",
Country("França", R.drawable.franca),
Country("Austrália", R.drawable.australia),
Date( inMilliseconds(16,7,0) ),
"Arena Kazan"
),
Game(
"D",
Country("Argentina", R.drawable.argentina),
Country("Islândia", R.drawable.islandia),
Date( inMilliseconds(16,10,0) ),
"Arena Otkrytie"
),
Game(
"C",
Country("Peru", R.drawable.peru),
Country("Dinamarca", R.drawable.dinamarca),
Date( inMilliseconds(16,13,0) ),
"Arena Mordovia"
),
Game(
"D",
Country("Croácia", R.drawable.croacia),
Country("Nigéria", R.drawable.nigeria),
Date( inMilliseconds(16,16,0) ),
"Kaliningrad Stadium"
),
Game(
"E",
Country("Costa Rica", R.drawable.costa_rica),
Country("Sérvia", R.drawable.servia),
Date( inMilliseconds(17,9,0) ),
"Estádio de Samara"
),
Game(
"F",
Country("Alemanha", R.drawable.alemanha),
Country("México", R.drawable.mexico),
Date( inMilliseconds(17,12,0) ),
"Luzhniki"
),
Game(
"E",
Country("Brasil", R.drawable.brasil),
Country("Suíça", R.drawable.suica),
Date( inMilliseconds(17,15,0) ),
"Arena Rostov"
),
Game(
"F",
Country("Suécia", R.drawable.suecia),
Country("Coreia do Sul", R.drawable.coreia_do_sul),
Date( inMilliseconds(18,9,0) ),
"Níjni Novgorod"
),
Game(
"G",
Country("Bélgica", R.drawable.belgica),
Country("Panamá", R.drawable.panama),
Date( inMilliseconds(18,12,0) ),
"Olímpico de Fisht"
),
Game(
"G",
Country("Tunísia", R.drawable.tunisia),
Country("Inglaterra", R.drawable.inglaterra),
Date( inMilliseconds(18,15,0) ),
"Arena Volgogrado"
),
Game(
"H",
Country("Colômbia", R.drawable.colombia),
Country("Japão", R.drawable.japao),
Date( inMilliseconds(19,9,0) ),
"Arena Mordovia"
),
Game(
"H",
Country("Polônia", R.drawable.polonia),
Country("Senegal", R.drawable.senegal),
Date( inMilliseconds(19,12,0) ),
"Arena Otkrytie"
)
)

/*
* Método responsável por converter a data do jogo em milissegundos,
* pois essa é a entrada aceita em Date.
* */
private fun inMilliseconds(day: Int, hourOfDay: Int, minute: Int) : Long {
val calendar = Calendar.getInstance()
calendar.set(2018,6,day,hourOfDay,minute)
return calendar.timeInMillis
}
}
}

 

Thiengo, mas não seria melhor colocar as funções de Database como funções de alto nível no projeto, assim poderíamos dispensar o uso da classe Database e economizar em linhas de código?

Na verdade não, a principio manter a classe Database traz uma melhor leitura de código por parte de qualquer developer Kotlin.

Como recomendação: as questões críticas de performance, que necessitam a quebra do código bem escrito em prol da velocidade, devem ser preocupantes somente quando o aplicativo começa a apresentar problemas de lentidão.

Classe adaptadora de listagem de jogos

Para a classe GamesAdapter, vamos iniciar com o layout de item, /res/layout/game.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorItemOdd"
android:paddingBottom="19dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="19dp"
tools:context="thiengo.com.br.fifaworldcup2018.MainActivity">

<ImageView
android:id="@+id/iv_country_a"
android:layout_width="104dp"
android:layout_height="62dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/tv_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_country_a"
android:layout_centerHorizontal="true"
android:layout_marginTop="4dp"
android:textColor="@android:color/white"
android:textSize="12sp" />

<TextView
android:id="@+id/tv_vs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_group"
android:layout_centerHorizontal="true"
android:layout_marginTop="-3dp"
android:text="@string/vs"
android:textColor="@android:color/white"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_date_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_vs"
android:layout_centerHorizontal="true"
android:layout_marginTop="-4dp"
android:textColor="@android:color/white"
android:textSize="12sp" />

<TextView
android:id="@+id/tv_stadium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_date_time"
android:layout_centerHorizontal="true"
android:layout_marginTop="-4dp"
android:textColor="@android:color/white"
android:textSize="12sp" />

<ImageView
android:id="@+id/iv_country_b"
android:layout_width="104dp"
android:layout_height="62dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:scaleType="fitCenter" />
</RelativeLayout>

 

Assim o diagrama do layout anterior:

Diagrama do layout game.xml

Agora o código Kotlin de GamesAdapter:

class GamesAdapter(
private val context: Context,
private val games: List<Game>) :
RecyclerView.Adapter<GamesAdapter.ViewHolder>() {

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

val v = LayoutInflater
.from(context)
.inflate(R.layout.game, parent, false)
return ViewHolder(v)
}

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

holder.setData( games[position] )

/*
* Código para a mudança de cor de background de item.
* Algoritmo utilizado, pois é mais simples do que
* utilizar o terceiro parâmetro de onCreateViewHolder()
* e assim trabalhar com dois layouts.
* */
holder.itemView.setBackgroundColor(
if( position % 2 == 0 )
ContextCompat.getColor(context, R.color.colorItemEven)
else
ContextCompat.getColor(context, R.color.colorItemOdd)
)
}

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

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

var ivCountryA: ImageView
var ivCountryB: ImageView
var tvGroup: TextView
var tvDateTime: TextView
var tvStadium: TextView

init {
ivCountryA = itemView.findViewById(R.id.iv_country_a)
ivCountryB = itemView.findViewById(R.id.iv_country_b)
tvGroup = itemView.findViewById(R.id.tv_group)
tvDateTime = itemView.findViewById(R.id.tv_date_time)
tvStadium = itemView.findViewById(R.id.tv_stadium)
}

fun setData( game: Game ) {
ivCountryA.setImageResource( game.countryA.flagResource )
ivCountryA.contentDescription = game.countryA.name
ivCountryB.setImageResource( game.countryB.flagResource )
ivCountryB.contentDescription = game.countryB.name

tvGroup.text = game.generateGroupLabel()
tvDateTime.text = game.generateDateTimeLabel()
tvStadium.text = game.stadium
}
}
}

 

Assim podemos partir para a parte final desta primeira jornada de desenvolvimento do app, a atividade principal.

Atividade de listagem de jogos

Para a MainActivity vamos iniciar com o XML do layout, /res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rv_games"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimaryDark" />

 

Como o layout é bem simples, com a definição de somente um RecyclerView, vamos partir direto para o código Kotlin da atividade:

class MainActivity : AppCompatActivity() {

var games: ArrayList<Game>? = null

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

/* Simulando uma busca custosa de dados remotos. */
games = Database.getGames()

rv_games.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(this)
rv_games.layoutManager = layoutManager
rv_games.adapter = GamesAdapter(this, games!!)
}
}

 

Assim podemos partir para a melhoria do projeto, retendo os dados de games caso haja necessidade de reconstrução de atividade, isso, pois os dados são pouco dinâmicos.

Integração com a API Android-State

Como informado anteriormente: nosso aplicativo tem uma busca custosa de dados que são pouco dinâmicos, assim temos de trabalhar o mínimo de performance retendo esses dados caso haja a reconstrução da atividade principal.

Dentre todas as soluções possíveis, vamos prosseguir com o uso da API Android-State, pois é de código simples e atende a nossa necessidade.

Instalação da API via Gradle App Level

No Gradle App Level, ou build.gradle (Module: app), adicione os códigos em destaque:

...
dependencies {
...

/* ANDROID STATE */
implementation 'com.evernote:android-state:1.2.1'
kapt 'com.evernote:android-state-processor:1.2.1'
}

 

Sincronize o projeto. Lembre-se de sempre utilizar a versão mais atual da API, isso, pois a versão mais recente tende a conter melhorias consideráveis a biblioteca.

Atualizando as classes de domínio para implementar o Parcelable

Uma das necessidades de trabalho com a API Android-State é que os objetos possam ser colocados em uma instância de Bundle. Em caso de objetos diferentes de String há a necessidade de implementação das Interfaces Serializable ou Parcelable.

Aqui vamos seguir com a implementação do Parcelable, pois o processamento é bem mais eficiente do que a outra opção, apesar da necessidade de mais código, necessidade que facilmente é suprida com a adição de um plugin Parcelable.

Segue o novo código da classe Country:

class Country(
var name: String,
val flagResource: Int) : Parcelable {

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

override fun describeContents() = 0

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

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

 

Então o novo código da classe Game:

class Game(
val group: String,
val countryA: Country,
val countryB: Country,
val dateTime: Date,
val stadium: String) : Parcelable {

fun generateGroupLabel() = "Grupo $group"

fun generateDateTimeLabel(): String {
var gameTime = "${dateTime.getDayOfMonthLabel()}/"
gameTime += dateTime.getMonthLabel()
gameTime += " às ${dateTime.getHourOfDayLabel()}:"
gameTime += dateTime.getMinuteLabel()

return gameTime
}

constructor(source: Parcel) : this(
source.readString(),
source.readParcelable<Country>(Country::class.java.classLoader),
source.readParcelable<Country>(Country::class.java.classLoader),
source.readSerializable() as Date,
source.readString()
)

override fun describeContents() = 0

override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
writeString(group)
writeParcelable(countryA, 0)
writeParcelable(countryB, 0)
writeSerializable(dateTime)
writeString(stadium)
}

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

 

Note que a classe Date nativa já implementa a Interface Serializable.

Colocando StateSaver e @State na atividade principal

Como temos somente uma atividade, seria desnecessário criar uma nova classe Application somente para reter o estado das instâncias dessa única atividade. Devido a isso vamos seguir, na MainActivity, com a estratégia onde o uso explícito de StateSaver junto aos métodos restoreInstanceState() e saveInstanceState() é necessário.

Segue novo código de MainActivity:

class MainActivity : AppCompatActivity() {

@State
var games: ArrayList<Game>? = null

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

if( games == null ){
/* Para verificação no momento do teste. */
Log.i("LOG", "A lista games estava vazia.")
games = Database.getGames()
}

rv_games.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(this)
rv_games.layoutManager = layoutManager
rv_games.adapter = GamesAdapter(this, games!!)
}

override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
StateSaver.saveInstanceState( this, outState!! )
}
}

 

Com isso podemos partir para o teste.

Teste e resultado

Executando o aplicativo e então verificando os logs do Android Studio, temos:

Animação de teste da Android-State API

A API Android-State funcionou como esperado, a lista de jogos foi retida em memória e uma nova criação custosa de objetos não foi realizada. Assim ficamos por aqui com a apresentação da API Android-State.

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

Se inscreva também no canal do Blog em: YouTube Thiengo.

Slides

Abaixo os slides com a apresentação completa da API Android-State:

Vídeo

Abaixo o vídeo com a implementação passo a passo da API Android-State no projeto de aplicativo de confrontos da Copa do Mundo FIFA de 2018:

Para acesso completo ao projeto FIFA World Cup 2018, entre no GitHub a seguir: https://github.com/viniciusthiengo/fifa-world-cup-2018.

Conclusão

Apesar da simplicidade da Android-State API é necessário levar em consideração ao menos a quantidade de dados que terão de ser mantidos em memória.

Caso a lista de jogos do aplicativo de exemplo apresentasse todos os jogos da fase de grupos, seguramente uma melhor solução seria o uso do ViewModel, isso pela quantidade de objetos em memória.

Para casos de aplicativos com dados estáticos, como os apps de dieta e de portfólio de músicos e artistas, para esses casos, por exemplo, a library Android-State tende a ser uma melhor opção.

Comente abaixo suas dúvidas e o que achou da API. Indique também as soluções conhecidas por ti para retenção de objetos em atividades, fragmentos e visualizações.

Não deixe de se inscrever na 📩 lista de e-mails para receber em primeira mão os conteúdos exclusivos do Blog.

Abraço.

Fontes

Documentação Evernote Android-State library

Documentação Icepick library

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
BottomNavigationView Android, Como e Quando UtilizarBottomNavigationView Android, Como e Quando UtilizarAndroid
Chips Android, Quando e Como UtilizarChips Android, Quando e Como UtilizarAndroid
Trabalhando Análise Qualitativa em seu Aplicativo AndroidTrabalhando Análise Qualitativa em seu Aplicativo AndroidAndroid

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