Adotar o Kotlin para equipes grandes

Mudar para qualquer nova linguagem pode ser uma tarefa assustadora. A receita para o sucesso é começar devagar, fazer mudanças por partes e testar com frequência para alinhar a equipe em busca do sucesso. O Kotlin facilita a migração, porque se baseia no bytecode JVM e é totalmente interoperável com o Java.

Como montar a equipe

A primeira etapa antes da migração é criar um entendimento básico comum para a equipe. Veja algumas dicas que podem ser úteis para acelerar o aprendizado das equipes.

Formar grupos de estudo

Grupos de estudo são uma maneira eficaz de facilitar a aprendizagem e a absorção. Estudos sugerem (link em inglês) que recitar o que você aprendeu em um grupo ajuda a reforçar o material. Compre um livro sobre Kotlin (link em inglês) ou outro material de estudo para cada participante e peça ao grupo para estudar alguns capítulos por semana. Durante cada reunião, o grupo pode comparar o que aprendeu e fazer perguntas ou observações.

Construir uma cultura de aprendizagem

Embora nem todo mundo se considere professor, todos podem transmitir conhecimento. De um líder de tecnologia ou equipe a um colaborador individual, todos podem incentivar um ambiente de aprendizagem que pode ajudar a garantir o sucesso. Uma maneira de facilitar isso é fazer apresentações periódicas, em que uma pessoa da equipe é designada para falar sobre algo que aprendeu ou que gostaria de compartilhar. Você pode aproveitar o grupo de estudo e pedir que voluntários apresentem um novo capítulo a cada semana até chegar a um ponto em que a equipe se sinta confortável com a linguagem.

Indicar uma liderança

Por fim, indique uma liderança para conduzir o processo de aprendizagem. Essa pessoa poderá atuar como um especialista no assunto (SME, na sigla em inglês) quando você iniciar o processo de adoção. É importante incluir essa pessoa em todas as reuniões práticas relacionadas ao Kotlin. O ideal é que essa pessoa já esteja envolvida com o Kotlin e tenha algum conhecimento prático.

Integrar lentamente

É fundamental começar devagar e pensar estrategicamente sobre quais partes do seu ecossistema precisam mudar primeiro. Muitas vezes, é melhor isolar esse processo em um app único na sua organização, em vez de em um app principal. Em termos de migração do app escolhido, cada situação é diferente, mas estes são alguns pontos comuns para começar.

Modelo de dados

Provavelmente, seu modelo de dados consiste em muitas informações de estado e alguns métodos. O modelo de dados também pode ter métodos comuns, como toString(), equals() e hashcode(). Em geral, esses métodos podem ser transferidos e testados em isolamento com facilidade.

Por exemplo, considere o seguinte snippet de Java:

public class Person {

   private String firstName;
   private String lastName;
   // ...

   public String getFirstName() {
       return firstName;
   }

   public void setFirstName(String firstName) {
       this.firstName = firstName;
   }

   public String getLastName() {
       return lastName;
   }

   public void setLastName(String lastName) {
       this.lastName = lastName;
   }

   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Person person = (Person) o;
       return Objects.equals(firstName, person.firstName) &&
               Objects.equals(lastName, person.lastName);
   }

   @Override
   public int hashCode() {
       return Objects.hash(firstName, lastName);
   }

   @Override
   public String toString() {
       return "Person{" +
               "firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               '}';
   }
}

Você pode substituir a classe Java por uma única linha de Kotlin, conforme mostrado aqui:

data class Person(var firstName: String?, var lastName : String?)

Esse código pode passar por um teste de unidade em relação ao seu conjunto de testes atual. A ideia aqui é começar aos poucos, com um modelo por vez e com as classes de transição que são principalmente de estado, não de comportamento. Faça testes com frequência ao longo do processo.

Migrar testes

Outro caminho inicial a ser considerado é converter os testes atuais e começar a criar novos testes em Kotlin. Assim, sua equipe tem tempo para se sentir confortável com a linguagem antes de escrever o código que você planeja enviar com o app.

Mover métodos utilitários para funções de extensão

Todas as classes de utilitários estáticos (StringUtils, IntegerUtils, DateUtils, YourCustomTypeUtils e assim por diante) podem ser representadas como funções de extensão do Kotlin e usadas pela base de código Java atual.

Por exemplo, considere que você tem uma classe StringUtils com alguns métodos:

package com.java.project;

public class StringUtils {

   public static String foo(String receiver) {
       return receiver...;  // Transform the receiver in some way
   }

   public static String bar(String receiver) {
       return receiver...;  // Transform the receiver in some way
   }

}

Esses métodos podem ser usados em outro lugar no app, como mostrado no exemplo a seguir:

...

String myString = ...
String fooString = StringUtils.foo(myString);

...

Usando as funções de extensão do Kotlin, você pode fornecer a mesma interface Utils para autores de chamada do Java e, ao mesmo tempo, oferecer uma API mais sucinta para a crescente base do código Kotlin.

Para fazer isso, você pode começar convertendo essa classe Utils em Kotlin usando a conversão automática fornecida pelo ambiente de desenvolvimento integrado. A saída de exemplo pode ser semelhante a esta:

package com.java.project

object StringUtils {

   fun foo(receiver: String): String {
       return receiver...;  // Transform the receiver in some way
   }

   fun bar(receiver: String): String {
       return receiver...;  // Transform the receiver in some way
   }

}

Em seguida, remova a definição de classe ou objeto, prefixe cada nome de função com o tipo em que essa função se aplica e use-a para fazer referência ao tipo dentro da função, como mostrado no exemplo a seguir:

package com.java.project

fun String.foo(): String {
    return this...;  // Transform the receiver in some way
}

fun String.bar(): String {
    return this...;  // Transform the receiver in some way
}

Por fim, adicione uma anotação JvmName à parte superior do arquivo fonte para tornar o nome compilado compatível com o restante do app, como mostrado no exemplo a seguir:

@file:JvmName("StringUtils")
package com.java.project
...

A versão final se parecerá com esta:

@file:JvmName("StringUtils")
package com.java.project

fun String.foo(): String {
    return this...;  // Transform `this` string in some way
}

fun String.bar(): String {
    return this...;  // Transform `this` string in some way
}

Essas funções agora podem ser chamadas usando Java ou Kotlin com convenções que correspondem a cada linguagem.

Kotlin

...
val myString: String = ...
val fooString = myString.foo()
...

Java

...
String myString = ...
String fooString = StringUtils.foo(myString);
...

Concluir a migração

Quando a equipe estiver familiarizada com o Kotlin e você tiver migrado áreas menores, poderá começar a processar componentes maiores, como fragmentos, atividades, objetos ViewModel e outras classes relacionadas à lógica de negócios.

Considerações

Assim como o Java tem um estilo específico, o Kotlin tem seu próprio estilo idiomático que contribui para que ele seja sucinto. Inicialmente, no entanto, talvez você perceba que o código Kotlin que sua equipe produz se parece mais com o código Java que ele está substituindo. Isso muda com o tempo, à medida que a experiência da equipe com o Kotlin aumenta. Lembre-se de que a mudança gradual é a chave para o sucesso.

Veja algumas coisas que você pode fazer para conseguir consistência à medida que a base do código Kotlin crescer:

Padrões comuns de programação

Defina um conjunto padrão de convenções de programação logo no início do processo de adoção. Você pode divergir do guia de estilo Kotlin do Android onde fizer sentido.

Ferramentas de análise estática

Aplique os padrões de codificação definidos para sua equipe usando o Android lint e outras ferramentas de análise estática. O klint, um linter de Kotlin de terceiros, também fornece outras regras para Kotlin.

Integração contínua

Siga padrões de programação comuns e forneça cobertura de teste suficiente para o código Kotlin. Fazer isso, em um processo de criação automatizado, pode ajudar a garantir a consistência e a aderência a esses padrões.

Interoperabilidade

O Kotlin interopera com o Java perfeitamente na maior parte do tempo, mas observe o seguinte.

Nulidade

O Kotlin depende de anotações de nulidade no código compilado para inferir a capacidade de anulação no lado do Kotlin. Se as anotações não forem fornecidas, o Kotlin adotará, por padrão, um tipo de plataforma que pode ser tratado como o tipo anulável ou não anulável. No entanto, isso pode levar a problemas de NullPointerException na execução se não for tratado com cuidado.

Adotar novos recursos

O Kotlin oferece muitas bibliotecas novas e açúcar sintático para reduzir o código boilerplate, o que ajuda a aumentar a velocidade de desenvolvimento. Dito isso, tenha uma abordagem cautelosa e metódica ao usar as funções de biblioteca padrão do Kotlin, como funções de coleção, corrotinas e lambdas (links em inglês).

Aqui está uma armadilha muito comum que os desenvolvedores mais novos do Kotlin encontram. Considere o seguinte código Kotlin:

val nullableFoo: Foo? = ...

// This lambda executes only if nullableFoo is not null
// and `foo` is of the non-nullable Foo type
nullableFoo?.let { foo ->
   foo.baz()
   foo.zap()
}

A intenção, neste exemplo, é executar foo.baz() e foo.zap() se nullableFoo não for nulo, evitando um NullPointerException. Embora esse código funcione como esperado, a leitura dele é menos intuitiva do que uma simples verificação de objetos nulos e transmissão inteligente (link em inglês), como mostrado no exemplo a seguir:

val nullableFoo: Foo? = null
if (nullableFoo != null) {
    nullableFoo.baz() // Using !! or ?. isn't required; the Kotlin compiler infers non-nullability
    nullableFoo.zap() // from guard condition; smart casts nullableFoo to Foo inside this block
}

Teste

Por padrão, as classes e as funções são fechadas para extensão no Kotlin. É necessário abrir explicitamente as classes e funções de que você quer criar subclasses. Esse comportamento é uma decisão de design da linguagem escolhida para promover a composição em vez da herança. O Kotlin tem suporte integrado para implementar o comportamento por meio da delegação para ajudar a simplificar a composição.

Esse comportamento representa um problema para frameworks de simulação, como o Mockito, que dependem de implementação de interface ou herança para substituir comportamentos durante o teste. Para testes de unidade, ative o recurso Mock Maker Inline do Mockito, que permite simular classes e métodos finais. Você também pode usar o plug-in do compilador All-Open para abrir qualquer classe Kotlin e os membros que você quer testar como parte do processo de compilação. A principal vantagem de usar esse plug-in é que ele funciona com testes instrumentados e de unidade.

Mais informações

Para ver mais informações sobre como usar o Kotlin, confira os links a seguir: