segunda-feira, 22 de maio de 2017

Architecture Components do Android

Olá povo,

O Google I/O 2017 foi repleto de novidades fantásticas para a plataforma Android. Entre elas, duas foram fundamentais para os desenvolvedores Android: Kotlin e Architecture Components.

Apesar de podermos desenvolver aplicativos Android utilizando Kotlin nas versões anteriores do Android Studio por meio de um plugin instalado à parte, agora ao criar um projeto poderemos selecionar a linguagem de nossa preferência (Java ou Kotlin).

Os Architecture Components vieram para facilitar a implementação de aplicativos de uma forma mais robusta e padronizada, e nesse post vou mostrar um exemplo simples de como utilizar essas APIs.


Adicionando as dependências

Para implementar esse projeto, eu utilizei o Android Studio 2.4 com o plugin 1.1.2 do Kotlin. Mas se quiser utilizar a versão preview do Android Studio 3.0 eu creio que você não terá muitos problemas. Crie um novo projeto com suporte a Kotlin e nomeie a sua activity principal como ListPeopleActivity. Com o projeto criado, vamos fazer as seguintes mudanças no build.gradle do seu projeto como a seguir:

buildscript {
    ext.gradle_version = '2.3.2'
    ext.kotlin_version = '1.1.2-3'
    ext.anko_version = '0.8.2'
    ext.support_version = '25.3.1'
    ext.arch_lifecycle_version = "1.0.0-alpha1"
    ext.arch_room_version = "1.0.0-alpha1"

    repositories {
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradle_version"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
        mavenCentral()
    }
}
Aqui criamos uma série de variáveis com as versões das bibliotecas que utilizaremos no projeto. Isso não é obrigatório, mas facilita a legibilidade do arquivo e evita erros e conflitos entre versões das bibliotecas. Adicionamos a versão do Gradle, do Kotlin, do Anko (que é uma biblioteca para facilitar o acesso aos componentes de UI), da support library e finalmente, das bibliotecas de arquitetura (lifecycle e room).
Na lista de dependências, além do gradle, adicionamos o plugin do Kotlin. Por fim, na lista de repositórios, devemos adicionar o "maven.google.com".

Agora vá até o build.gradle do módulo da sua aplicação e faça os seguintes ajustes.

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

android {
    // Nada muda aqui...
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile "org.jetbrains.anko:anko-common:$anko_version"

    compile "com.android.support:appcompat-v7:$support_version"
    compile "com.android.support:design:$support_version"
    compile "com.android.support:support-v4:$support_version"

    compile "android.arch.lifecycle:runtime:$arch_lifecycle_version"
    compile "android.arch.lifecycle:extensions:$arch_lifecycle_version"
    compile "android.arch.persistence.room:runtime:$arch_room_version"

    kapt "android.arch.lifecycle:compiler:$arch_lifecycle_version"
    kapt "android.arch.persistence.room:compiler:$arch_room_version"
}
Além do plugin do Android, estamos aplicando o plugin do Kotlin e o de extensões para o Kotlin. Na seção de dependências, adicionamos todas as bibliotecas que utilizaremos nesse projeto. O kapt (Kotlin Annotation Process Tool) substitui o "annotationProcessor" para projeto sem Kotlin. Perceba que utilizamos o kapt para as bibliotecas de arquitetura, pois ela geram o código em tempo de compilação. Isso deve ser feito para outras bibliotecas que fazem esse trabalho (Dagger, DataBinding,  etc.).

Definindo o acesso ao banco

Para simplificar o tutorial e facilitar o entendimento, nosso aplicativo será um cadastro simples de pessoas, onde cada pessoa possuirá o primeiro nome, sobrenome e idade. Para tal, definiremos a classe a seguir:

import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey
import java.io.Serializable

@Entity
data class Person(
        @PrimaryKey(autoGenerate = true) var id: Long = 0L,
        var firstName: String = "",
        var lastName: String = "",
        var age: Int = 0) : Serializable
Criamos uma data class anotada com @Entity, o que indica que objetos dessa classe serão persistidos. Todos os parâmetros têm valores default, então podemos criar objetos dessa classe passando nenhum parâmetro. Uma outra curiosidade nessa classe é que devemos adicionar um atributo como @PrimaryKey, nesse exemplo, ele será gerado automaticamente, então definimos a propriedade autoGenerate para true.

Agora vamos definiremos a DAO, a interface que possuirá os métodos para interagir com o banco de dados.
import android.arch.lifecycle.LiveData
import android.arch.persistence.room.*
import android.arch.persistence.room.OnConflictStrategy.IGNORE

@Dao
interface PeopleDao {

    @Insert(onConflict = IGNORE)
    fun insertPerson(person: Person)

    @Update
    fun updatePerson(person: Person)

    @Delete
    fun deletePerson(vararg people: Person)

    @Query("SELECT * FROM Person ORDER BY firstName")
    fun listAll(): LiveData<List<Person>>
}
Nossa interface está anotada com @Dao e os métodos de inserir, atualizar, excluir e listar estão anotados respectivamente com @Insert, @Update, @Delete e @Query. O método insertPerson(Person) possui em sua anotação a propriedade onConflict definida como IGNORE, indicando que caso haja um conflito no momento da inserção o registro será ignorado.
Apesar de não estarmos utilizando esse recurso, no método deletePerson() permitimos excluir vários registros ao mesmo tempo utilizando um vararg. A parte mais interessante é o método listAll() que retorna um LiveData de uma lista de objetos Person.
Utilizando o LiveData quando qualquer registro for inserido, alterado ou excluído, a lista será automaticamente atualizada.
O último passo para podermos utilizar o Room, é definir uma classe que herda de RoomDatabase como mostraremos a seguir.
import android.arch.persistence.room.Database
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import android.content.Context

@Database(
        entities = arrayOf(Person::class),
        version = 1)
abstract class AppDatabase : RoomDatabase() {

    abstract fun peopleDao(): PeopleDao

    companion object {

        private val DB_NAME = "dbPeople"
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase? {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                            context.applicationContext,
                            AppDatabase::class.java,
                            DB_NAME)
                        .allowMainThreadQueries()
                        .build()
            }
            return INSTANCE
        }

        fun destroyInstance() {
            INSTANCE = null
        }
    }
}
Na anotação @Database informamos quais entidades serão persistidas utilizando a propriedade "entities" e a versão do banco. Perceba que essa é uma classe abstrata, pois assim como a nossa interface DAO, a implementação será gerada em tempo de compilação.
Essa classe será um singleton que é inicializado por meio do método Room.databaseBuilder onde passamos o contexto, a referência à nossa classe e o nome do banco. Perceba que estamos utilizando o método allowMainThreadQueries() que não é recomendado, pois devemos realizar as consultas em uma thread separada, mas não estamos fazendo isso aqui para simplificar o exemplo.
O método peopleDao() retornará uma instância da implementação do nosso DAO.

Definindo os ViewModels

Definida nossa camada de acesso ao banco, partiremos agora para a definição dos ViewModels do nosso pequeno projeto. Primeiramente, vamos criar uma classe que será utilizada na tela de listagem e será responsável por manter o resultado da busca durante a rotação da tela do aparelho sem ter que fazer uma nova consulta ao banco.
import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData

class ListPeopleViewModel(app : Application) : AndroidViewModel(app) {

    var livePeople: LiveData<List<Person>>? = null
    var db: PeopleDao? = null

    private fun getDao() : PeopleDao? {
        if (db == null) {
            db = AppDatabase.getDatabase(getApplication())?.peopleDao()
        }
        return db
    }

    fun getPeople(): LiveData<List<Person>> {
        if (livePeople == null) {
            livePeople = getDao()?.listAll()
        }
        return livePeople as LiveData<List<Person>>
    }
}
Nossa classe herda de AndroidViewModel e está mantendo a LiveData da lista de Person. Dessa forma, caso a lista seja nula, carregamos do banco, caso contrário apenas a retornamos.
Faremos uma classe similar para a tela de cadastro, mas apenas para isolar o acesso ao banco da nossa UI.
import android.app.Application
import android.arch.lifecycle.AndroidViewModel

class PersonFormViewModel(app : Application) : AndroidViewModel(app) {

    private var db: PeopleDao? = null

    private fun peopleDao() : PeopleDao? {
        if (db == null) {
            db = AppDatabase.getDatabase(getApplication())?.peopleDao()
        }
        return db
    }

    fun savePerson(person : Person) {

        if (person.id == 0L) {
            peopleDao()?.insertPerson(person)

        } else {
            peopleDao()?.updatePerson(person)
        }
    }

    fun deletePerson(person: Person) {
        peopleDao()?.deletePerson(person)
    }
}
O método savePerson(Person) irá inserir o objeto se o seu id for igual a zero ou atualizá-lo caso contrário. Já o método deletePerson(Person) excluirá o objeto Person.

Juntando tudo

Finalmente vamos implementar as telas da aplicação, começando pela tela de listagem.
import android.arch.lifecycle.LifecycleRegistry
import android.arch.lifecycle.LifecycleRegistryOwner
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_list_people.*

class ListPeopleActivity : AppCompatActivity(), LifecycleRegistryOwner {

    var lifecycleRegistry = LifecycleRegistry(this)

    override fun getLifecycle(): LifecycleRegistry = lifecycleRegistry

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

        fab.setOnClickListener {
            PersonFormFragment().show(supportFragmentManager, "form")
        }

        val model = ViewModelProviders.of(this).get(ListPeopleViewModel::class.java)
        model.getPeople().observe(this, Observer { people ->
            listview.adapter = ArrayAdapter<Person>(
                    this@ListPeopleActivity,
                    android.R.layout.simple_list_item_1,
                    people)

            listview.setOnItemClickListener { _, _, position, _ ->
                val person = people?.get(position)
                if (person != null) {
                    PersonFormFragment.newInstance(person).show(supportFragmentManager, "form")
                }
            }
        })
    }

    override fun onDestroy() {
        AppDatabase.destroyInstance()
        super.onDestroy()
    }
}
Nossa classe herda de AppCompatActivity e implementa LifecycleRegistryOwner. Essa interface requer que implementemos o método getLifecycle() que retorna um objeto LifecycleRegistry. Poderíamos simplesmente herdade de LifecycleActivity, mas essa classe herda de FragmentActivity, então perderíamos a ActionBar padrão que é adicionada.

Não vou mostrar o layout dessa activity aqui, mas ele possui apenas uma ListView com o id @+id/listview e uma FloatingActionButton com o id @+id/fab. Perceba que estamos acessando os componentes de UI usando simplesmente o seu id. Isso é feito graças ao kotlinx (Kotlin Extensions).

Ao clicar no FAB, o PersonFormFragment (que mostraremos a seguir) é exibido. Perceba que estamos obtendo o ViewModel dessa tela por meio da classe ViewModelProviders e passando o nome da classe.
Com o ViewModel, obtemos a lista de pessoas e chamamos o método observe para sabermos quando a listagem mudou. Nesse caso, definimos um adapter para o nosso listView e atribuímos o evento de click no item. Perceba que como não estamos utilizando todos os parâmetros, estamos utilizando o "_" no lugar dos respectivos nomes.

A última classe é tela de cadastro que é um DialogFragment.
import android.arch.lifecycle.ViewModelProviders
import android.content.Context
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_person_form.*

class PersonFormFragment : DialogFragment() {

    lateinit var person : Person
    lateinit var viewModel : PersonFormViewModel

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        viewModel = ViewModelProviders.of(this).get(PersonFormViewModel::class.java)
        person = arguments?.getSerializable(EXTRA_PERSON) as? Person ?: Person()
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return inflater?.inflate(R.layout.fragment_person_form, container, false)
    }

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        edtFirstName.setText(person.firstName)
        edtLastName.setText(person.lastName)
        edtAge.setText(if (person.age != 0 || person.id != 0L) person.age.toString() else "")

        btnRemove.visibility = if (person.id == 0L) View.GONE else View.VISIBLE

        btnCancel.setOnClickListener { dismiss() }
        btnSave.setOnClickListener { savePerson() }
        btnRemove.setOnClickListener { removePerson() }
    }

    private fun removePerson() {
        viewModel.deletePerson(person)
        dismiss()
    }

    private fun savePerson() {
        person.firstName = edtFirstName.text.toString()
        person.lastName = edtLastName.text.toString()
        person.age = edtAge.text.toString().toInt()

        viewModel.savePerson(person)
        dismiss()
    }

    companion object {
        val EXTRA_PERSON = "person"

        fun newInstance(person: Person) : PersonFormFragment {
            val args = Bundle()
            args.putSerializable(EXTRA_PERSON, person)

            val f = PersonFormFragment()
            f.arguments = args
            return f
        }
    }
}
Mais uma vez estamos omitindo o arquivo de layout aqui, mas ele possui basicamente três EditText (edtFirstName, edtLastName e edtAge) para os respectivos campos da classe Person, e três botões (btnSave, btnRemove, btnCancel) para salvar, remover e cancelar o dialog. Sendo que o segundo só estará habilitado se estivermos editando uma pessoa.
No onAttach estamos inicializando nosso ViewModel e obtendo o objeto Person passado da tela de listagem. Se não tivermos passado esse objeto, criamos um novo. Perceba que nessa instrução estamos usando "?" nos arguments, pois ele pode ser null, no "as?" pois não pode ser feito um cast de null para Person (isso se chama save cast) e por fim utilizamos o "elvis operator", pois caso a instrução anterior seja nula, criamos um novo objeto Person.
Nada de especial no onCreateView, mas no onViewCreated temos apenas uma instrução interessante para quem é novo em Kotlin que é o if/else retornando um valor (seria o mesmo que o ternário do Java "?:").
Nos métodos removePerson chamamos nosso ViewModel para excluir o objeto e no savePerson para salvar o objeto. Por fim, nossa classe possui o "factory method" newInstance para passarmos o objeto pessoa como parâmetro para a tela de cadastro no caso de uma alteração.

O resultado deve ficar como a seguir:


Essa foi uma pequena introdução de como utilizar as bibliotecas de arquitetura lançadas no Google IO de 2017 utilizando Kotlin. Obviamente existe muito mais a explorar, mas espero que esse post já ajude a dar os primeiros passos com essas APIs.

Para saber mais, assistam esses vídeos do Google IO e a documentação dessas libs. ;)

Architecture Components - Introduction
https://www.youtube.com/watch?v=FrteWKKVyzI

Architecture Components - Solving the Lifecycle Problem
https://www.youtube.com/watch?v=bEKNi1JOrNs

Architecture Components - Persistence and Offline
https://www.youtube.com/watch?v=MfHsPGQ6bgE

Documentação oficial
https://developer.android.com/topic/libraries/architecture/index.html

4br4ç05,
nglauber

4 comentários:

Levi Saturnino disse...

Olá Glauber, já tem pretensão de sai a nova versão do livro(dominando Android) com essas novidades, principalmente os códigos totalmente em kotlin ao invés de java? Abs.

Nelson Glauber disse...

Oi Levi,
Por enquanto nenhuma previsão do livro. Estava trabalhando nele, mas ainda estava no começo e chegaram umas demandas do trabalho e acabou atrapalhando os planos um pouco.
Mas espero que a nova edição tenha essas novidades sim.

4br4ç05,
nglauber

Alex Leiva disse...

Olá Glauber. Obrigado pelo artigo.
Se possível, gostaria de saber uma coisa em relação a esta novidade:
como eu posso definir o tamanho de um campo, por exemplo string, se ele vai ser nulo ou não e outras particularidades de um campo?

Obrigado pela ajuda.
Grande abraço.

Nelson Glauber disse...

Oi Alex,

No SQLite não definimos o tamanho do campo, apenas seu tipo: TEXT, INTEGER, REAL ou BLOB. Já se ele será nulo ou não, por padrão ele aceitará valores null, caso não queira que o campo seja "not null" você pode usar a anotação @NonNull.
Você pode definir o nome da coluna utilizando @ColumnInfo, ou criar um índice com @Index.

4br4ç05,
nglauber