Introdução ao Kotlin: programação Android para humanos
Publicados: 2022-03-11Em um mundo Android perfeito, a linguagem principal do Java é realmente moderna, clara e elegante. Você pode escrever menos fazendo mais, e sempre que um novo recurso aparece, os desenvolvedores podem usá-lo apenas aumentando a versão no Gradle. Então, ao criar um aplicativo muito bom, ele parece totalmente testável, extensível e sustentável. Nossas atividades não são muito grandes e complicadas, podemos alterar as fontes de dados do banco de dados para a web sem muitas diferenças e assim por diante. Parece ótimo, certo? Infelizmente, o mundo Android não é o ideal. O Google ainda busca a perfeição, mas todos sabemos que mundos ideais não existem. Assim, temos que nos ajudar nessa grande jornada no mundo Android.
O que é Kotlin e por que você deve usá-lo?
Então, a primeira língua. Acho que Java não é o mestre da elegância ou clareza, e não é moderno nem expressivo (e acho que você concorda). A desvantagem é que abaixo do Android N, ainda estamos limitados ao Java 6 (incluindo algumas pequenas partes do Java 7). Os desenvolvedores também podem anexar o RetroLambda para usar expressões lambda em seu código, o que é muito útil ao usar o RxJava. Acima do Android N, podemos usar algumas das novas funcionalidades do Java 8, mas ainda é aquele Java antigo e pesado. Muitas vezes, ouço desenvolvedores do Android dizerem: “Gostaria que o Android suportasse uma linguagem melhor, como o iOS faz com o Swift”. E se eu lhe disser que você pode usar uma linguagem muito agradável e simples, com segurança nula, lambdas e muitos outros novos recursos interessantes? Bem-vindo ao Kotlin.
O que é Kotlin?
Kotlin é uma nova linguagem (às vezes chamada de Swift para Android), desenvolvida pela equipe JetBrains, e agora está em sua versão 1.0.2. O que o torna útil no desenvolvimento Android é que ele compila para bytecode JVM e também pode ser compilado para JavaScript. É totalmente compatível com Java, e o código Kotlin pode ser simplesmente convertido em código Java e vice-versa (há um plugin da JetBrains). Isso significa que o Kotlin pode usar qualquer framework, biblioteca etc. escrito em Java. No Android, integra-se pelo Gradle. Se você tem um aplicativo Android existente e deseja implementar um novo recurso em Kotlin sem reescrever todo o aplicativo, basta começar a escrever em Kotlin e ele funcionará.
Mas quais são os 'grandes novos recursos'? Deixe-me listar alguns:
Parâmetros de função opcionais e nomeados
fun createDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0, second: Int = 0) { print("TEST", "$day-$month-$year $hour:$minute:$second") }Podemos chamar o método createDate de maneiras diferentes
createDate(1,2,2016) prints: '1-2-2016 0:0:0' createDate(1,2,2016, 12) prints: '1-2-2016 12:0:0' createDate(1,2,2016, minute = 30) prints: '1-2-2016 0:30:0'Segurança nula
Se uma variável puder ser nula, o código não será compilado, a menos que os forcemos a fazê-lo. O código a seguir terá um erro - nullableVar pode ser nulo:
var nullableVar: String? = “”; nullableVar.length;Para compilar, temos que verificar se não é nulo:
if(nullableVar){ nullableVar.length }Ou, mais curto:
nullableVar?.lengthDessa forma, se nullableVar for null, nada acontece. Caso contrário, podemos marcar a variável como não anulável, sem um ponto de interrogação após o tipo:
var nonNullableVar: String = “”; nonNullableVar.length;Este código compila, e se quisermos atribuir null a nonNullableVar, o compilador mostrará um erro.
Há também um operador Elvis muito útil:
var stringLength = nullableVar?.length ?: 0Então, quando nullableVar é nulo (portanto, nullableVar?.length retorna nulo), stringLength terá o valor 0.
Variáveis mutáveis e imutáveis
No exemplo acima, eu uso var ao definir uma variável. Isso é mutável, podemos reatribuí-lo sempre que quisermos. Se queremos que essa variável seja imutável (em muitos casos deveríamos), usamos val (como valor, não variável):
val immutable: Int = 1Depois disso, o compilador não nos permitirá reatribuir para imutável.
Lambdas
Todos nós sabemos o que é um lambda, então aqui vou mostrar como podemos usá-lo em Kotlin:
button.setOnClickListener({ view -> Log.d("Kotlin","Click")})Ou se a função for o único ou último argumento:
button.setOnClickListener { Log.d("Kotlin","Click")}Extensões
As extensões são um recurso de linguagem muito útil, graças ao qual podemos “estender” as classes existentes, mesmo quando elas são definitivas ou não temos acesso ao seu código-fonte.
Por exemplo, para obter um valor de string do texto de edição, em vez de escrever toda vez que editText.text.toString(), podemos escrever a função:
fun EditText.textValue(): String{ return text.toString() }Ou mais curto:
fun EditText.textValue() = text.toString()E agora, com cada instância de EditText:
editText.textValue()Ou podemos adicionar uma propriedade retornando o mesmo:
var EditText.textValue: String get() = text.toString() set(v) {setText(v)}Sobrecarga do operador
Às vezes útil se quisermos adicionar, multiplicar ou comparar objetos. Kotlin permite a sobrecarga de operadores binários (plus, minus, plusAssign, range, etc.), operadores de array (get, set, get range, set range) e operações iguais e unárias (+a, -a, etc.)
Classe de dados
Quantas linhas de código você precisa para implementar uma classe User em Java com três propriedades: copy, equals, hashCode e toString? No Kaotlin você precisa apenas de uma linha:
data class User(val name: String, val surname: String, val age: Int)Essa classe de dados fornece os métodos equals(), hashCode() e copy(), e também toString(), que imprime User como:
User(name=John, surname=Doe, age=23)As classes de dados também fornecem algumas outras funções e propriedades úteis, que você pode ver na documentação do Kotlin.
Extensões Anko
Você usa extensões Butterknife ou Android, não é? E se você nem precisar usar essa biblioteca e, depois de declarar exibições em XML, apenas use-a do código por seu ID (como com XAML em C#):
<Button android: android:layout_width="match_parent" android:layout_height="wrap_content" /> loginBtn.setOnClickListener{}O Kotlin possui extensões Anko muito úteis, e com isso você não precisa informar à sua atividade o que é loginBtn, ele sabe apenas “importando” xml:
import kotlinx.android.synthetic.main.activity_main.*Existem muitas outras coisas úteis no Anko, incluindo iniciar atividades, mostrar brindes e assim por diante. Este não é o objetivo principal do Anko - ele foi projetado para criar layouts facilmente a partir do código. Portanto, se você precisar criar um layout programaticamente, essa é a melhor maneira.
Esta é apenas uma visão curta do Kotlin. Recomendo a leitura do blog de Antonio Leiva e seu livro - Kotlin for Android Developers e, claro, o site oficial do Kotlin.
O que é MVP e por quê?
Uma linguagem agradável, poderosa e clara não é suficiente. É muito fácil escrever aplicativos confusos com todos os idiomas sem uma boa arquitetura. Os desenvolvedores do Android (principalmente aqueles que estão começando, mas também os mais avançados) geralmente atribuem responsabilidade à Activity por tudo ao seu redor. Atividade (ou Fragmento, ou outra visualização) baixa dados, envia para salvar, apresenta-os, responde às interações do usuário, edita dados, gerencia todas as visualizações filhas. . . e muitas vezes muito mais. É demais para objetos tão instáveis como Atividades ou Fragmentos (basta girar a tela e a Atividade diz 'Adeus...').
Uma ideia muito boa é isolar as responsabilidades das visões e torná-las o mais estúpidas possível. As Views (Activities, Fragments, Custom Views, ou o que quer que apresente dados na tela) devem ser responsáveis apenas por gerenciar suas subviews. As visualizações devem ter apresentadores, que se comunicarão com o modelo e informarão o que eles devem fazer. Este, em suma, é o padrão Model-View-Presenter (para mim, deve ser nomeado Model-Presenter-View para mostrar as conexões entre as camadas).
“Ei, eu conheço algo assim, e se chama MVC!” - você não achou? Não, MVP não é o mesmo que MVC. No padrão MVC, sua view pode se comunicar com model. Ao usar o MVP, você não permite nenhuma comunicação entre essas duas camadas - a única maneira de a View se comunicar com o Model é através do Presenter. A única coisa que View sabe sobre Model pode ser a estrutura de dados. View sabe como, por exemplo, exibir User, mas não sabe quando. Aqui está um exemplo simples:
View sabe “Sou Activity, tenho dois EditTexts e um Button. Quando alguém clica no botão, devo dizer ao meu apresentador e passar os valores de EditTexts. E isso é tudo, posso dormir até o próximo clique ou o apresentador me dizer o que fazer.”
O Presenter sabe que em algum lugar existe uma View e sabe quais operações essa View pode realizar. Ele também sabe que ao receber duas strings, ele deve criar User a partir dessas duas strings e enviar os dados para o modelo salvar, e se o salvamento for bem sucedido, informar a view 'Show success info'.
O modelo apenas sabe onde os dados estão, onde eles devem ser salvos e quais operações devem ser executadas nos dados.
Os aplicativos escritos em MVP são fáceis de testar, manter e reutilizar. Um apresentador puro não deve saber nada sobre a plataforma Android. Deve ser uma classe Java pura (ou Kotlin, no nosso caso). Graças a isso, podemos reutilizar nosso apresentador em outros projetos. Também podemos facilmente escrever testes de unidade, testando separadamente Model, View e Presenter.
Uma pequena digressão: o MVP deve fazer parte da Arquitetura Limpa do Uncle Bob para tornar os aplicativos ainda mais flexíveis e bem arquitetados. Vou tentar escrever sobre isso da próxima vez.
Aplicativo de exemplo com MVP e Kotlin
Isso é teoria suficiente, vamos ver algum código! Ok, vamos tentar criar um aplicativo simples. O objetivo principal para este aplicativo é criar usuário. A primeira tela terá dois EditTexts (Nome e Sobrenome) e um Botão (Salvar). Após inserir nome e sobrenome e clicar em 'Salvar', o aplicativo deve mostrar 'Usuário salvo' e ir para a próxima tela, onde é apresentado o nome e o sobrenome salvos. Quando o nome ou sobrenome está vazio, o aplicativo não deve salvar o usuário e mostrar um erro indicando o que está errado.
A primeira coisa depois de criar o projeto Android Studio é configurar o Kotlin. Você deve instalar o plugin Kotlin e, após reiniciar, em Ferramentas > Kotlin você pode clicar em 'Configurar Kotlin no projeto'. O IDE adicionará dependências Kotlin ao Gradle. Se você tiver algum código existente, poderá convertê-lo facilmente para Kotlin por (Ctrl+Shift+Alt+K ou Código > Converter arquivo Java para Kotlin). Se algo está errado e o projeto não compila, ou o Gradle não vê o Kotlin, você pode verificar o código do aplicativo disponível no GitHub.
Agora que temos um projeto, vamos começar criando nossa primeira visualização - CreateUserView. Essa visão deve ter as funcionalidades mencionadas anteriormente, para que possamos escrever uma interface para isso:

interface CreateUserView : View { fun showEmptyNameError() /* show error when name is empty */ fun showEmptySurnameError() /* show error when surname is empty */ fun showUserSaved() /* show user saved info */ fun showUserDetails(user: User) /* show user details */ }Como você pode ver, Kotlin é semelhante ao Java na declaração de funções. Todas essas são funções que não retornam nada, e a última tem um parâmetro. Esta é a diferença, o tipo de parâmetro vem depois do nome. A interface do View não é do Android - é nossa interface simples e vazia:
interface ViewA interface do Basic Presenter deve ter uma propriedade do tipo View, e pelo menos no método (onDestroy por exemplo), onde esta propriedade será definida como null:
interface Presenter<T : View> { var view: T? fun onDestroy(){ view = null } }Aqui você pode ver outro recurso Kotlin - você pode declarar propriedades em interfaces e também implementar métodos lá.
Nosso CreateUserView precisa se comunicar com CreateUserPresenter. A única função adicional que este apresentador precisa é saveUser com dois argumentos de string:
interface CreateUserPresenter<T : View>: Presenter<T> { fun saveUser(name: String, surname: String) }Também precisamos de definição de modelo - é mencionado anteriormente classe de dados:
data class User(val name: String, val surname: String)Depois de declarar todas as interfaces, podemos começar a implementar.
CreateUserPresenter será implementado em CreateUserPresenterImpl:
class CreateUserPresenterImpl(override var view: CreateUserView?): CreateUserPresenter<CreateUserView> { override fun saveUser(name: String, surname: String) { } }A primeira linha, com definição de classe:
CreateUserPresenterImpl(override var view: CreateUserView?)É um construtor, nós o usamos para atribuir a propriedade view, definida na interface.
MainActivity, que é nossa implementação CreateUserView, precisa de uma referência para CreateUserPresenter:
class MainActivity : AppCompatActivity(), CreateUserView { private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) saveUserBtn.setOnClickListener{ presenter.saveUser(userName.textValue(), userSurname.textValue()) /*use of textValue() extension, mentioned earlier */ } } override fun showEmptyNameError() { userName.error = getString(R.string.name_empty_error) /* it's equal to userName.setError() - Kotlin allows us to use property */ } override fun showEmptySurnameError() { userSurname.error = getString(R.string.surname_empty_error) } override fun showUserSaved() { toast(R.string.user_saved) /* anko extension - equal to Toast.makeText(this, R.string.user_saved, Toast.LENGTH_LONG) */ } override fun showUserDetails(user: User) { } override fun onDestroy() { presenter.onDestroy() } }No início da aula, definimos nosso apresentador:
private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) }Ele é definido como imutável (val) e é criado por um delegado preguiçoso, que será atribuído na primeira vez que for necessário. Além disso, temos certeza de que não será nulo (sem ponto de interrogação após a definição).
Quando o usuário clica no botão Salvar, o View envia informações para o Presenter com valores de EditTexts. Quando isso acontece, User deve ser salvo, então temos que implementar o método saveUser no Presenter (e algumas das funções do Model):
override fun saveUser(name: String, surname: String) { val user = User(name, surname) when(UserValidator.validateUser(user)){ UserError.EMPTY_NAME -> view?.showEmptyNameError() UserError.EMPTY_SURNAME -> view?.showEmptySurnameError() UserError.NO_ERROR -> { UserStore.saveUser(user) view?.showUserSaved() view?.showUserDetails(user) } } }Quando um usuário é criado, ele é enviado ao UserValidator para verificar a validade. Então, de acordo com o resultado da validação, o método apropriado é chamado. A construção when() {} é igual a switch/case em Java. Mas é mais poderoso - Kotlin permite o uso não apenas de enum ou int em 'case', mas também de intervalos, strings ou tipos de objetos. Deve conter todas as possibilidades ou ter uma expressão else. Aqui, abrange todos os valores de UserError.
Ao usar view?.showEmptyNameError() (com um ponto de interrogação após a exibição), estamos protegidos contra NullPointer. A visão pode ser anulada no método onDestroy, e com essa construção, nada acontecerá.
Quando um modelo de usuário não tem erros, ele informa ao UserStore para salvá-lo e, em seguida, instrui o View a mostrar o sucesso e os detalhes.
Como mencionado anteriormente, temos que implementar algumas coisas de modelo:
enum class UserError { EMPTY_NAME, EMPTY_SURNAME, NO_ERROR } object UserStore { fun saveUser(user: User){ //Save user somewhere: Database, SharedPreferences, send to web... } } object UserValidator { fun validateUser(user: User): UserError { with(user){ if(name.isNullOrEmpty()) return UserError.EMPTY_NAME if(surname.isNullOrEmpty()) return UserError.EMPTY_SURNAME } return UserError.NO_ERROR } }A coisa mais interessante aqui é o UserValidator. Usando a palavra objeto, podemos criar uma classe singleton, sem preocupações com threads, construtores privados e assim por diante.
A próxima coisa - no método validateUser(user), existe a expressão with(user) {}. O código dentro de tal bloco é executado no contexto do objeto, passado com nome e sobrenome são propriedades do usuário.
Há também outra pequena coisa. Todo o código acima, de enum a UserValidator, a definição é colocada em um arquivo (a definição da classe User também está aqui). Kotlin não força você a ter cada classe pública em um único arquivo (ou nomear a classe exatamente como arquivo). Assim, se você tiver alguns pequenos pedaços de código relacionado (classes de dados, extensões, funções, constantes - Kotlin não requer classe para função ou constante), você pode colocá-lo em um único arquivo em vez de espalhar por todos os arquivos no projeto.
Quando um usuário é salvo, nosso aplicativo deve exibir isso. Precisamos de outra visualização - pode ser qualquer visualização do Android, visualização personalizada, fragmento ou atividade. Eu escolhi Atividade.
Então, vamos definir a interface UserDetailsView. Ele pode mostrar o usuário, mas também deve mostrar um erro quando o usuário não estiver presente:
interface UserDetailsView { fun showUserDetails(user: User) fun showNoUserError() }Em seguida, UserDetailsPresenter. Deve ter uma propriedade de usuário:
interface UserDetailsPresenter<T: View>: Presenter<T> { var user: User? }Esta interface será implementada em UserDetailsPresenterImpl. Ele deve substituir a propriedade do usuário. Toda vez que essa propriedade é atribuída, o usuário deve ser atualizado na visualização. Podemos usar um setter de propriedades para isso:
class UserDetailsPresenterImpl(override var view: UserDetailsView?): UserDetailsPresenter<UserDetailsView> { override var user: User? = null set(value) { field = value if(field != null){ view?.showUserDetails(field!!) } else { view?.showNoUserError() } } }A implementação do UserDetailsView, UserDetailsActivity, é muito simples. Assim como antes, temos um objeto apresentador criado por carregamento lento. O usuário a ser exibido deve ser passado via intent. Há um pequeno problema com isso por enquanto, e vamos resolvê-lo em um momento. Quando temos o usuário da intenção, o View precisa atribuí-lo ao apresentador. Depois disso, o usuário será atualizado na tela, ou, se for nulo, o erro aparecerá (e a atividade terminará - mas o apresentador não sabe disso):
class UserDetailsActivity: AppCompatActivity(), UserDetailsView { private val presenter: UserDetailsPresenter<UserDetailsView> by lazy { UserDetailsPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_details) val user = intent.getParcelableExtra<User>(USER_KEY) presenter.user = user } override fun showUserDetails(user: User) { userFullName.text = "${user.name} ${user.surname}" } override fun showNoUserError() { toast(R.string.no_user_error) finish() } override fun onDestroy() { presenter.onDestroy() } }A passagem de objetos por meio de intents requer que esse objeto implemente a interface Parcelable. Este é um trabalho muito 'sujo'. Pessoalmente, odeio fazer isso por causa de todos os CRIADORES, propriedades, salvamento, restauração e assim por diante. Felizmente, existe um plugin adequado, Parcelable for Kotlin. Depois de instalá-lo, podemos gerar Parcelable apenas com um clique.
A última coisa a fazer é implementar showUserDetails(user: User) em nossa MainActivity:
override fun showUserDetails(user: User) { startActivity<UserDetailsActivity>(USER_KEY to user) /* anko extension - starts UserDetailsActivity and pass user as USER_KEY in intent */ }E isso é tudo.
Temos um aplicativo simples que salva um usuário (na verdade, ele não é salvo, mas podemos adicionar essa funcionalidade sem tocar no apresentador ou na visualização) e o apresenta na tela. No futuro, se quisermos alterar a forma como o usuário é apresentado na tela, como de duas atividades para dois fragmentos em uma atividade, ou duas visualizações personalizadas, as alterações serão apenas nas classes View. Claro, se não mudarmos a funcionalidade ou a estrutura do modelo. O apresentador, que não sabe exatamente o que é View, não precisará de nenhuma alteração.
Qual é o próximo?
Em nosso aplicativo, o Presenter é criado toda vez que uma atividade é criada. Essa abordagem, ou seu oposto, se o Presenter persistir nas instâncias de atividade, é um assunto de muita discussão na Internet. Para mim, depende do aplicativo, de suas necessidades e do desenvolvedor. Às vezes é melhor destruir o apresentador, às vezes não. Se você decidir persistir um, uma técnica muito interessante é usar o LoaderManager para isso.
Como mencionado anteriormente, o MVP deve fazer parte da arquitetura Clean do Uncle Bob. Além disso, bons desenvolvedores devem usar o Dagger para injetar dependências de apresentadores em atividades. Também ajuda a manter, testar e reutilizar código no futuro. Atualmente, Kotlin funciona muito bem com Dagger (antes do lançamento oficial não era tão fácil), e também com outras bibliotecas Android úteis.
Embrulhar
Para mim, Kotlin é uma ótima linguagem. É moderno, claro e expressivo enquanto ainda está sendo desenvolvido por grandes pessoas. E podemos usar qualquer nova versão em qualquer dispositivo e versão Android. O que quer que me deixe irritado com Java, Kotlin melhora.
Claro, como eu disse nada é o ideal. Kotlin também tem algumas desvantagens. As versões mais recentes do plugin gradle (principalmente de alfa ou beta) não funcionam bem com esta linguagem. Muitas pessoas reclamam que o tempo de compilação é um pouco maior do que Java puro, e os apks têm alguns MB adicionais. Mas o Android Studio e o Gradle ainda estão melhorando e os telefones têm cada vez mais espaço para aplicativos. É por isso que acredito que Kotlin pode ser uma linguagem muito legal para todo desenvolvedor Android. Basta experimentá-lo e compartilhar na seção de comentários abaixo o que você pensa.
O código-fonte do aplicativo de exemplo está disponível no Github: github.com/tomaszczura/AndroidMVPKotlin
