Como Utilizar os Operadores IN e Elvis e a API Parcelable no Kotlin Android

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação. Você receberá um email de confirmação. Somente depois de confirmar é que poderei lhe enviar o conteúdo exclusivo por email.

Email inválido.
Blog /Android /Como Utilizar os Operadores IN e Elvis e a API Parcelable no Kotlin Android

Como Utilizar os Operadores IN e Elvis e a API Parcelable no Kotlin Android

Vinícius Thiengo21/06/2017
(610) (1) (393) (18)
Go-ahead
"O início de um hábito é como um fio invisível, mas a cada vez que o repetimos o ato reforça o fio, acrescenta-lhe outro filamento, até que se torna um enorme cabo, e nos prende de forma irremediável, no pensamento e ação."
Orison Swett Marden
Código limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas598
Comprar Livro
Conteúdo Exclusivo
Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Opa, tudo bem?

Neste artigo vamos estudar duas das principais APIs Android, Parcelable e SavedInstanceState, e dois operadores novos para nós desenvolvedores Java, o operador "in" e o operador "Elvis" ou somente "?:".

Esses dois últimos, os operadores, nos permitem um esforço mínimo, respectivamente, na verificação de objeto em coleção e na certificação de que um valor null não seja utilizado.

Como em outros artigos, vamos abordar os assuntos informados em um domínio de problema real, dessa vez um aplicativo de venda de carros:

Aplicativo Android de carros

Antes de prosseguir é importante que você tenha ao menos a base no Kotlin Android. O conteúdo do artigo Kotlin Android, Entendendo e Primeiro Projeto pode lhe ajudar com isso caso você esteja conhecendo essa linguagem somente agora.

Este artigo também está disponível em vídeo, caso prefira este formato, siga direto para: Vídeo com implementação passo a passo das APIs e operadores.

A seguir os tópicos que estaremos estudando:

Projeto Android de exemplo

Caso queira ter acesso completo ao projeto que estaremos utilizando como exemplo, digo, incluindo arquivos de configuração do Android Studio e imagens, o que não é possível disponibilizar aqui, entre no GitHub a seguir: https://github.com/viniciusthiengo/carros-kotlin-app.

Nosso projeto é de um aplicativo de venda de carros importados. Ele será bem simples, o suficiente para conseguirmos apresentar as APIs e operadores propostos para o artigo. Mesmo assim teremos algo similar a um aplicativo real, um em produção.

Note que a partir deste ponto vou assumir que você sabe iniciar um projeto Kotlin Android caso esteja com uma versão do Android Studio inferior a versão 3.0 ou igual / superior a essa versão.

Assim, em sua instalação do Android Studio, crie um novo projeto com uma "Empty Activity" e com o seguinte nome: Carros à venda.

Ao final dessa primeira parte, digo, até antes das atualizações que aplicaremos ao app, teremos o seguinte aplicativo:

Atividade principal do aplicativo Android de carros

E a seguinte estrutura de projeto:

Estrutura de projeto Android do aplicativo de carros

Configurações Gradle

Ambos os arquivos gradle sofrerão alteração. O primeiro, Gradle Project Level, ou build.gradle (Project: CarrosKotlinApp), foi atualizado de acordo com as configurações de um projeto Kotlin. Segue:

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

allprojects {
repositories {
jcenter()
}
}

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

 

O Gradle App Level, ou build.gradle (Module: app), também teve atualizações, todas em destaque:

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

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

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
testCompile 'junit:junit:4.12'

/* PARA O RECYCLER VIEW */
compile 'com.android.support:design:25.3.1'

compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
repositories {
mavenCentral()
}

 

Não mais voltaremos a esses arquivos, pois nossas adições ao projeto serão todas em Kotlin code.

Configurações AndroidManifest

O AndroidManifest.xml se manterá o mesmo desde a criação do novo projeto no Android Studio:

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

<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=".CarrosActivity">
<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

As configurações de estilo são todas bem simples como em um novo projeto Empty Activity, somente adicionamos uma imagem de background para o aplicativo.

Vamos iniciar com o arquivo de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#4f4f4f</color>
<color name="colorPrimaryDark">#616161</color>
<color name="colorAccent">#607D8B</color>
</resources>

 

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

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Carros à venda</string>
</resources>

 

E por fim o arquivo de definição de estilo, /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat">
<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

As classes de domínio são bem simples, dispensando, por exemplo, os métodos getters e setters, digo, dispensando de modo explícito, pois no Kotlin esses métodos já estão presentes de forma implícita.

No pacote /domain do projeto, inicie adicionando a classe Acessorio:

class Acessorio (
val nome: String,
val preco: Float )

 

Em seguida vamos a adição da classe Motor:

class Motor(
val modelo: String,
val cilindros: Int,
val marca: String )

 

Agora a adição da classe Marca:

class Marca(
val nome: String,
val logo: Int)

 

Antes de prosseguir, note que nas classes anteriores, como não temos nenhum método de lógica proprietária para implementar, o corpo da classe não necessita ser expressado com chaves vazias, {}, podemos omitir essas.

Por fim a adição da classe de domínio principal, que conterá objetos das outras classes já apresentadas. Segue classe Carro:

class Carro(
val modelo: String,
val ano: Int,
val marca: Marca,
val motor: Motor,
val preco: Float,
val acessorios: List<Acessorio>,
val imagem: Bitmap ) {

fun getAcessoriosFormatted(): String {
val aux = StringBuilder()

for (acessorio in acessorios)
aux.append("${acessorio.nome} (${acessorio.preco}), ")

return aux.trimEnd().trimEnd(',').toString()
}
}

 

Dessa vez temos uma função proprietária, getAcessoriosFormatted(), que tem o objetivo de colocar todos os dados da lista acessorios como uma única String.

Note que o método trimEnd(), sem nenhum argumento definido, remove todos os espaços em branco do final da String em execução. Quando definido algum argumento, os caracteres desse tipo é que são removidos do final da String.

Camada de dados, classe Mock

Para simular um aplicativo em produção, porém em ambiente de desenvolvimento, é comum utilizarmos dados mock, ou, como também conhecido, dados simulados.

Para isso teremos um estrutura mock, mais precisamente uma classe mock que será responsável por nos fornecer os objetos necessários para preencher o layout.

Segue código da classe Mock do pacote /data:

class Mock {
private fun gerarMotor() : Motor {
val modelos = arrayOf("V-Tec", "Rocan", "Zar-T")
val cilindros = arrayOf(4, 4, 8)
val marcas = arrayOf("Volkswagen", "Ford", "GM")
val randIndex = Random().nextInt(3)

return Motor( modelos[randIndex], cilindros[randIndex], marcas[randIndex])
}


private fun gerarAcessorio() : Acessorio {
val nomes = arrayOf("Teto solar", "Multimídia", "Aro 21 (Sport)", "Bancos de couro")
val precos = arrayOf(2500f, 5600f, 8000f, 980f)
val randIndex = Random().nextInt(4)

return Acessorio( nomes[randIndex], precos[randIndex] )
}


private fun gerarListaAcessorios() : List<Acessorio> {
val acessorios = LinkedList<Acessorio>()
val randIndex = Random().nextInt(3) + 1

while( acessorios.size < randIndex ){
val aux = gerarAcessorio()

if( !inAcessorios( aux, acessorios ) ){
acessorios.add( aux )
}
}

return acessorios
}

private fun inAcessorios( acessorio: Acessorio, acessorios : List<Acessorio> ) : Boolean{
for( aux in acessorios )
if( aux.nome == acessorio.nome )
return true
return false
}


private fun gerarBitmap( resources: Resources, imagemRes: Int ) : Bitmap {
return BitmapFactory.decodeResource( resources, imagemRes )
}


fun gerarCarro( resources: Resources ) : List<Carro> {

val carros = listOf(
Carro(
"Impala",
2014,
Marca("Chevrolet", R.drawable.chevrolet_logo),
gerarMotor(),
89_997f,
gerarListaAcessorios(),
gerarBitmap(resources, R.drawable.chevrolet_impala) ),
Carro(
"Evoque",
2017,
Marca("Land Rover", R.drawable.land_rover_logo),
gerarMotor(),
228_500f,
gerarListaAcessorios(),
gerarBitmap(resources, R.drawable.land_rover_evoque) ),
Carro(
"Toureg",
2017,
Marca("Volkswagen", R.drawable.volkswagen_logo),
gerarMotor(),
327_793f,
gerarListaAcessorios(),
gerarBitmap(resources, R.drawable.volkswagen_toureg) ),
Carro(
"Fusion",
2017,
Marca("Ford", R.drawable.ford_logo),
gerarMotor(),
98_650f,
gerarListaAcessorios(),
gerarBitmap(resources, R.drawable.ford_fusion) ),
Carro(
"Taurus",
2015,
Marca("Ford", R.drawable.ford_logo),
gerarMotor(),
113_985f,
gerarListaAcessorios(),
gerarBitmap(resources, R.drawable.ford_taurus) )
)

return carros
}
}

 

Dê uma atenção especial ao método inAcessorios():

...
private fun inAcessorios( acessorio: Acessorio, acessorios : List<Acessorio> ) : Boolean {
for( aux in acessorios )
if( aux.nome == acessorio.nome )
return true
return false
}
...

 

Pois com um único operador Kotlin seguramente esse método poderá ser descartado.

Classe adaptadora

Como nosso aplicativo trabalha com lista de carros, tivemos de escolher um framework de lista do Android. Optamos pelo RecyclerView, isso por ele ser mais atual e o mais recomendado quando pensando também em melhor performance.

Vamos iniciar com o layout de item que utilizaremos na classe adaptadora. Segue XML de /res/layout/item_carro.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp">

<ImageView
android:id="@+id/iv_imagem"
android:layout_width="140dp"
android:layout_height="90dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:scaleType="centerCrop" />

<TextView
android:id="@+id/tv_modelo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_imagem"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_toEndOf="@+id/iv_imagem"
android:layout_toRightOf="@+id/iv_imagem"
android:ellipsize="end"
android:maxLines="2"
android:textSize="18sp" />

<ImageView
android:id="@+id/iv_logo"
android:layout_width="28dp"
android:layout_height="18dp"
android:layout_alignLeft="@+id/tv_modelo"
android:layout_alignStart="@+id/tv_modelo"
android:layout_below="@+id/tv_modelo"
android:layout_marginTop="6dp"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/tv_marca"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_modelo"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:layout_marginTop="6dp"
android:layout_toEndOf="@+id/iv_logo"
android:layout_toRightOf="@+id/iv_logo"
android:textSize="13sp" />

<TextView
android:id="@+id/tv_motor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_modelo"
android:layout_alignStart="@+id/tv_modelo"
android:layout_below="@+id/iv_logo"
android:layout_marginTop="6dp"
android:bufferType="spannable"
android:textSize="13sp" />

<TextView
android:id="@+id/tv_acessorios"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_modelo"
android:layout_alignStart="@+id/tv_modelo"
android:layout_below="@+id/tv_motor"
android:layout_marginTop="6dp"
android:bufferType="spannable"
android:textSize="13sp" />

<TextView
android:id="@+id/tv_preco"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_imagem"
android:layout_marginTop="4dp"
android:textColor="#ccc"
android:textSize="19sp"
android:textStyle="bold" />
</RelativeLayout>

 

A seguir o diagrama do layout anterior:

Layout XML Android de item_carro.xml

Assim o Kotlin code da classe adaptadora CarrosAdapter, presente no package /adapter:

class CarrosAdapter(
private val context: Context,
private val carros: List<Carro>) :
RecyclerView.Adapter<CarrosAdapter.ViewHolder>() {

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

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

return ViewHolder(v)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setData(carros[position])
}

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


inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var ivImagem: ImageView
var ivLogo: ImageView
var tvModelo: TextView
var tvMarca: TextView
var tvMotor: TextView
var tvAcessorios: TextView
var tvPreco: TextView

init {
ivImagem = itemView.findViewById(R.id.iv_imagem) as ImageView
ivLogo = itemView.findViewById(R.id.iv_logo) as ImageView
tvModelo = itemView.findViewById(R.id.tv_modelo) as TextView
tvMarca = itemView.findViewById(R.id.tv_marca) as TextView
tvMotor = itemView.findViewById(R.id.tv_motor) as TextView
tvAcessorios = itemView.findViewById(R.id.tv_acessorios) as TextView
tvPreco = itemView.findViewById(R.id.tv_preco) as TextView
}

fun setData(carro: Carro) {
ivImagem.setImageBitmap(carro.imagem)
tvModelo.text = carro.modelo
ivLogo.setImageResource(carro.marca.logo)
tvMarca.text = "${carro.marca.nome} - ${carro.ano}"
tvMotor.text = "Motor: ${carro.motor.modelo} (${carro.motor.cilindros}) - ${carro.motor.marca}"
tvAcessorios.text = "Acessórios: ${carro.getAcessoriosFormatted()}"
tvPreco.text = carro.preco.toString()
}
}
}

 

Espere um pouco. Você está utilizando o kotlin-extensions, por que há a necessidade de acessar os objetos Views pelo findViewById()?

Isso é necessário, pois, diferente de componentes onde teremos somente um objeto de layout sendo carregado, dentro de um adapter de framework de lista teremos mais do que somente um, logo não podemos assumir que o kotlin-extensions conseguirá acertar qual é o objeto de layout atual e simplesmente nos permitir acessar as Views corretas pelos IDs definidos.

Note que o inner utilizado na definição da classe ViewHolder é somente para permitir que a classe interna tenha acesso a uma instância da classe externa, CarrosAdapter.

Como em nosso domínio do problema, digo, a classe CarrosAdapter e ViewHolder compartilham o mesmo ciclo de vida, não há problemas em manter o uso do inner. Mas caso você não quisesse acesso a essa instância, poderia somente remover o uso dessa palavra chave, deixando apenas: class ViewHolder.

Atividade principal

Por fim a atividade principal do projeto. Vamos iniciar com o layout, /res/layout/activity_carros.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="match_parent"
tools:context="br.com.thiengo.carroskotlinapp.CarrosActivity">

<android.support.v7.widget.RecyclerView
android:id="@+id/rv_carros"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true" />

</RelativeLayout>

 

A seguir o simples diagrama do layout activity_carros.xml:

Layout XML Android de activity_carros.xml

Agora o código Kotlin da classe CarrosActivity, lembrando que essa classe está na raíz do projeto e não dentro de algum package específico:

class CarrosActivity : AppCompatActivity() {

val carros = ArrayList<Carro>()

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

carros.addAll( Mock().gerarCarro(resources) )
initRecycler()
}

private fun initRecycler() {
rv_carros.setHasFixedSize(true)

val mLayoutManager = LinearLayoutManager(this)
rv_carros.layoutManager = mLayoutManager

val divider = DividerItemDecoration(
this,
mLayoutManager.orientation)
rv_carros.addItemDecoration(divider)

val adapter = CarrosAdapter(this, carros)
rv_carros.adapter = adapter
}
}

 

Com isso podemos partir para as melhorias no aplicativo.

Atualizando o projeto

Antes de partirmos para as atualizações, precisamos saber o que atualizar:

  • Primeiro aquele código de verificação de acessórios, mais precisamente o método inAcessorios() da classe Mock, ele é ineficiente quando sabendo que temos uma opção nativa na linguagem que é mais viável;
  • Segundo, os preços apresentados em cada item de carro, digo, na tela, não estão formatados como dinheiro, não no formato utilizado no Brasil. Incluindo a isso o não destaque de rótulos e conteúdos nas linhas de "Motor" e "Acessórios", temos de atualizar esses também:

Item da lista de carros do aplicativo Android de carros

  • Terceiro e último, a lista de carros deve ser mantida, não temos de realizar a criação de objetos novamente somente porque a atividade foi reconstruída. Isso em produção, com a busca dos dados na Web, será ineficiente e utilizará muita banda de Internet do usuário, além do problema de vazamento de memória. Essa re-criação de objetos deverá ser melhorada.

Utilizando o operador "in"

Para nosso primeiro problema, o método ineficiente inAcessorios(), o Kotlin oferece o operador in. Na classe Mock, apague o método inAcessorios() e atualize o gerarListaAcessorios() como em destaque a seguir:

...
private fun gerarListaAcessorios() : List<Acessorio> {
val acessorios = LinkedList<Acessorio>()
val randIndex = Random().nextInt(3) + 1

while( acessorios.size < randIndex ){
val aux = gerarAcessorio()

if( aux !in acessorios ){
acessorios.add( aux )
}
}

return acessorios
}
...

 

Acredite, o operador in sendo utilizado com tipos de dados que permitam o uso do método contains(), implicitamente, somente nos solicita isso na codificação, digo, quase isso.

"quase isso"?

Sim, pois o método contains() que é utilizado de forma implícita, utiliza dentro dele a invocação ao método equals() do objeto. Porém, caso nós não tenhamos sobrescrito de alguma forma a implementação padrão desse método, o equals(), objetos com os mesmos valores de propriedades nunca serão considerados iguais em contains().

A implementação padrão de equals() vem da classe Any. Essa classe é herdada de forma implícita, direta ou indiretamente, por todas as classes Kotlin. Como ocorre com a classe Object em Java.

Se você executar o projeto como está, com nossa nova atualização, perceberá em alguns casos que acessórios iguais estão definidos para o mesmo carro, algo que foge do mínimo de consistência que esperamos neste aplicativo de exemplo.

Para corrigir isso, digo, fazer o operador in ser efetivo em nosso domínio do problema, temos duas opções:

  • Sobrescrevermos, na mão, o método equals();
  • Tornamos a classe Acessorio também uma data class, onde o método equals() já é sobrescrito, com o código dele utilizando somente as propriedades dos objetos na comparação.

Vamos optar pelo mais simples, tornar a classe Acessorio uma data class:

data class Acessorio (
val nome: String,
val preco: Float )

 

Assim, executando o aplicativo novamente, não mais haverá o problema de acessórios repetidos.

Trabalhando com funções estendidas em um Kotlin file específico

Para corrigirmos o problema de formatação de preço e de rótulo, vamos trabalhar com funções estendidas, pois objetos de diferentes classes necessitarão das mesmas novas funcionalidades.

Vamos separar os códigos de funções estendidas em um arquivo exclusivo, isso para facilitar a leitura do software como um todo.

Na raíz do projeto, crie um novo arquivo Kotlin, nomeado CustomExtensions.

Nosso primeiro código nele é para a formatação de preços em dados do tipo Float, o retorno deverá ser uma String com o valor em reais como utilizamos no Brasil. Segue:

fun Float.getPrecoHumam() = String.format( Locale.GERMAN, "R$ %,.2f", this )

 

Lembre que o Kotlin trabalha também com definição de tipo por inferência, logo, com nossa simples linha de código, podemos seguramente utilizar o formato CABEÇALHO = CORPO para a definição da função. Essa regra é válida também para funções novas que não são funções estendidas.

O argumento Locale.GERMAN indica que deve ser utilizado "." para separar os valores antes das casas decimais e vírgula, ",", para separar as casas decimais do valor inteiro.

A vírgula em "R$ %,.2f" na verdade diz que deve ser utilizado um separador, de acordo com o Locale definido, para o inteiro antes das casas decimais. O  ".2" indica que deve ter exatamente duas casas decimais depois do número inteiro.

Assim podemos partir para a segunda função estendia, a que permitirá colocarmos os rótulos em negrito para diferencia-los dos valores.

Segue nova função estendida na classe String (note que ainda estamos no arquivo CustomExtensions):

...
fun String.bold() : SpannableStringBuilder {
val aux = SpannableStringBuilder(this)

aux.setSpan(
StyleSpan(Typeface.BOLD),
0,
this.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)

return aux
}

 

Estamos utilizando uma SpannableStringBuilder, pois caso contrário teríamos de trabalhar a concatenação de String utilizando o operador "+", o que nos faria perder a formatação aplicada. Com SpannableStringBuilder podemos seguramente utilizar o método append().

Caso não conheça a SpannableString, depois deste artigo, não deixe de estudar o conteúdo completo que tenho sobre essa entidade e outras de estilização de String no Android: Como Utilizar Spannable no Android Para Customizar Strings.

Agora o que temos de fazer é atualizar os trechos que devem utilizar as novas funções estendidas.

Primeiro a classe Carro, mais precisamente o método getAcessoriosFormatted():

...
fun getAcessoriosFormatted(): String {
val aux = StringBuilder()

for (acessorio in acessorios)
aux.append("${acessorio.nome} (${acessorio.preco.getPrecoHumam()}), ")

return aux.trimEnd().trimEnd(',').toString()
}
...

 

Assim a classe CarrosAdapter, no método setData() atualize como está em destaque:

...
fun setData(carro: Carro) {
ivImagem.setImageBitmap(carro.imagem)
tvModelo.text = carro.modelo
ivLogo.setImageResource(carro.marca.logo)
tvMarca.text = "${carro.marca.nome} - ${carro.ano}"
tvMotor.text = "Motor: ".bold().append( "${carro.motor.modelo} (${carro.motor.cilindros}) - ${carro.motor.marca}" )
tvAcessorios.text = "Acessórios: ".bold().append( carro.getAcessoriosFormatted() )
tvPreco.text = carro.preco.getPrecoHumam()
}
...

 

Assim, o que ainda nos falta é o trabalho com o Parcelable e com o SavedInstanceState.

Configurando o plugin gerador de código da API Parcelable

Diferente de outras épocas, onde tínhamos de implementar o código da API Parcelable na mão em nossas classes, hoje, tanto para Java quanto para Kotlin, temos plugins que fazem isso para nós.

Com a janela do Android Studio aberta, clique em "Configure", logo depois clique em "Plugins":

Janela inicial do Android Studio

Na próxima janela clique em "Browse repositories...". Então, na nova janela, digite "parcelable" e selecione o plugin "Parcelable Code Generator (for kotlin)":

Janela de busca de plugin do Android Studio

Clique em "Install". Depois da instalação clique em "Restart Android Studio":

Janela de plugin no Android Studio

Depois do restart você terá o plugin configurado em seu AS.

Integrando Parcelable API às classes de domínio

Objetos de todas as nossas classes de domínio deverão ser mantidos em memória caso uma reconstrução de atividade, por exemplo, seja acionada. Logo, devemos implementar o Parcelable em todas as classes em /domain em nosso projeto de exemplo.

Vamos iniciar pelas menores. Abra a classe Marca, coloque o cursor do mouse em cima de alguma das propriedades dela e então clique com o botão direito dele. Em seguida clique em "Generate...", logo depois clique em "Parcelable(kotlin)":

Menu de contexto para Parcelable Kotlin

Com isso temos a classe Marca agora atualizada para ser enviada e buscada em um Parcel storage:

class Marca(
val nome: String,
val logo: Int) : Parcelable {

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

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

override fun describeContents() = 0

override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(nome)
dest.writeInt(logo)
}
}

 

O que é esse companion object?

Primeiro saiba que object é utilizado para estender classes sem necessidade de criar uma subclasse para isso. Ou seja, podemos colocar novas propriedades e funções às classes que escolhermos para isso.

O companion permite que essas entidades dentro do bloco de object possam ser acessadas com uma sintaxe similar a sintaxe de acesso a membro estático em Java, sem uso de instância.

Para forçarmos que um membro seja estático no momento da execução, devemos utilizar o annotation @JvmField.

Resumo: o que temos mais próximo, em Kotlin, de um static Java, é o companion object.

Com isso, o que ainda precisamos fazer é aplicar o método de geração de Parcelable em todas as outras classes.

A seguir o novo código da classe Acessorio:

data class Acessorio (
val nome: String,
val preco: Float ) : Parcelable {


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

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

override fun describeContents() = 0

override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(nome)
dest.writeFloat(preco)
}
}

 

Agora o novo código da classe Motor:

class Motor(
val modelo: String,
val cilindros: Int,
val marca: String ) : Parcelable {

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

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

override fun describeContents() = 0

override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(modelo)
dest.writeInt(cilindros)
dest.writeString(marca)
}
}

 

E por fim o novo código da classe Carro:

class Carro(
val modelo: String,
val ano: Int,
val marca: Marca,
val motor: Motor,
val preco: Float,
val acessorios: List<Acessorio>,
val imagem: Bitmap ) : Parcelable {

fun getAcessoriosFormatted(): String {
val aux = StringBuilder()

for (acessorio in acessorios)
aux.append("${acessorio.nome} (${acessorio.preco.getPrecoHumam()}), ")

return aux.trimEnd().trimEnd(',').toString()
}

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

constructor(source: Parcel) : this(
source.readString(),
source.readInt(),
source.readParcelable<Marca>(Marca::class.java.classLoader),
source.readParcelable<Motor>(Motor::class.java.classLoader),
source.readFloat(),
source.createTypedArrayList(Acessorio.CREATOR),
source.readParcelable<Bitmap>(Bitmap::class.java.classLoader)
)

override fun describeContents() = 0

override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(modelo)
dest.writeInt(ano)
dest.writeParcelable(marca, 0)
dest.writeParcelable(motor, 0)
dest.writeFloat(preco)
dest.writeTypedList(acessorios)
dest.writeParcelable(imagem, 0)
}
}

 

Assim podemos ir a atualização final do projeto.

Mantendo dados com a API SavedInstanceState e utilizando o operador "Elvis"

Para mantermos os objetos de nossa lista carros assim que uma reconstrução da CarrosActivity for solicitada, vamos utilizar os códigos de trabalho com o savedInstanceState.

Primeiro devemos sobrescrever o método onSaveInstanceState() na atividade principal, CarrosActivity:

...
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putParcelableArrayList( "carros", carros )
super.onSaveInstanceState(outState)
}
...

 

Note que a chave "carros" é um "valor mágico", literal de String que deverá ser utilizado mais de uma vez. Logo, vamos coloca-la como um companion em nossa classe Carro para dar mais sentido ao código e também facilitar uma possível atualização de chave no futuro:

class Carro(
val modelo: String,
val ano: Int,
val marca: Marca,
val motor: Motor,
val preco: Float,
val acessorios: List<Acessorio>,
val imagem: Bitmap ) : Parcelable {
...

companion object {
@JvmField val CARROS = "carros"
...
}
...
}

 

Assim, atualizando onSaveInstanceState() e colocando o código de recuperação de lista no onCreate() da CarrosActivity, temos:

class CarrosActivity : AppCompatActivity() {

val carros = ArrayList<Carro>()

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

carros.addAll(
savedInstanceState?.getParcelableArrayList( Carro.Companion.CARROS )
?: Mock().gerarCarro(resources)
)

...
}
...

override fun onSaveInstanceState(outState: Bundle?) {
outState?.putParcelableArrayList( Carro.Companion.CARROS, carros )
super.onSaveInstanceState(outState)
}
}

 

E então o Elvis, ou somente o operador ?:

O que acontece quando o operador ?: é utilizado é o seguinte: se a expressão a esquerda do operador retornar um null, então retorne a expressão a direita dele.

Em nosso caso a expressão a esquerda é: savedInstanceState?.getParcelableArrayList(Carro.Companion.CARROS). Tanto savedInstanceState quanto getParcelableArrayList() podem retornar null.

Nossa expressão a direita, Mock().gerarCarro(resources), é invocada somente na abertura do aplicativo e nunca retorna null. Isso, pois na abertura do aplicativo o savedInstanceState é nulo.

Note que resources em Kotlin é equivalente a getResources() em Java.

Com isso seguramente podemos partir para os testes.

Testes e resultados

Executando o aplicativo em um device de testes ou em um emulador, temos:

Atividade principal atualizada do app Android de carros

Note que os preços e rótulos estão formatados como desejado:

Item da lista de carros com os rótulos e valores formatados corretamente

Rotacionando a tela e verificando os acessórios, por exemplo, temos que nenhum objeto foi recriado:

Atividade principal atualizada e na horizontal do app Android de carros

Assim finalizamos mais esse conteúdo sobre o desenvolvimento de aplicativos Android com a nova linguagem Kotlin. Não deixe de se inscrever na lista de emails do Blog (ao final ou ao lado) para receber os conteúdos em primeira mão.

Se inscreva também no canal do Blog: YouTube Channel Thiengo [Calopsita].

Vídeo com implementação passo a passo das APIs e operadores

A seguir o vídeo com a implementação passo a passo das APIs e operadores que são assuntos no artigo:

Para acesso ao conteúdo completo do projeto, entre no GitHub a seguir: https://github.com/viniciusthiengo/carros-kotlin-app.

Conclusão

Com o Kotlin sendo uma linguagem moderna, operadores como o "in" e o "?:" são comuns e ajudam a diminuir e otimizar consideravelmente nossos algoritmos.

O uso do Parcelable e do SavedInstanceState no Android é necessário em muitos domínios do problema e os plugins disponíveis nos ajudam com a fácil implementação.

É importante ressaltar que mesmo tendo tanta facilidade na implementação de algoritmos por meio de simples operadores, isso não é sinônimo de ineficiente em tempo de execução. O Kotlin é no mínimo tão rápido quanto o Java, podendo ser até mais eficiente com o uso de funções anônimas.

Não deixe de comentar sua dúvida ou dica e também de se inscrever na lista de emails do Blog logo abaixo (ou ao lado).

Abraço.

Fontes

Kotlin reference: Object Expressions and Declarations

Kotlin reference: Calling Kotlin from Java - Static fields

Kotlin reference: Null Safety - Elvis operator

Kotlin reference: Nested Classes - Inner class

Kotlin reference: Operator overloading - In

Android documentation: Parcelable

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Relacionado

Parcelable no Android, Entendendo e UtilizandoParcelable no Android, Entendendo e UtilizandoAndroid
Como Utilizar Spannable no Android Para Customizar StringsComo Utilizar Spannable no Android Para Customizar StringsAndroid
Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
Iniciando com Anko Kotlin. Intenções no AndroidIniciando com Anko Kotlin. Intenções no AndroidAndroid

Compartilhar

Comentários Facebook

Comentários Blog (1)

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...
Marcelo (2) (0)
22/06/2017
Thiengo muito bom o conteúdo.  Estou estudando o Kotliin e seus videos ajudam bastante.

Gostaria de sugerir dois tópicos :

1. Faça um video falando de você.  

2. Faça um video falando sobre o Gradle


Ate+
Responder