Kotlin MultiPlataform Compartilhando código entre Android e iOS

A tecnologia Kotlin Multiplatform permite compartilhar códigos dos módulos domain(regras de negócio) e data(APIs) entre plataformas diferentes. Ou seja, compartilhar a lógica do aplicativo entre aplicativos iOS e Android por exemplo.
Para escrever um código específico do iOS e executar um aplicativo iOS em um dispositivo simulado ou real, é obrigatório um Mac com macOS. Isso não pode ser executado em outros sistemas operacionais, como Windows ou Linux.
5 passos são necessários para este exemplo:
- Primeiro passo: Configurar o ambiente
- Segundo passo: Criar um projeto multi plataforma
- Terceiro passo: Adicionar dependências ao projeto
- Quarto passo: Atualizar o aplicativo
- Quinto passo: Finalizar o projeto
Primeiro Passo: Configurar o ambient
Instale o brew no mac
Copie e cole o comando abaixo no terminal do MAC:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Prepare seu Mac
Tenha instalados os seguintes programas:
- Android Studio
- Xcode
- JDK (para checar sua versao digite no console : java -version)
- Plugin Kotlin Multiplatform Mobile
- Plugin Kotlin
Para instalar os plugins no Android Studio, va em Settings/Preferences | Plugins, procure no Marketplace por Kotlin Multiplatform Mobile, e instale
Para atualizar o plug-in, na tela de boas-vindas do Android Studio, selecione Plug-ins | Instalados. Clique em Atualizar ao lado de Kotlin.
Para checar se tudo está correto e preparado instale o KDOCTOR
Digite o seguinte comando no terminal do MAC:
brew install kdoctor
Após rode o KDOCTOR digitando o comando:
kdoctor

Meu Xcode ainda não tinha terminado a instalação e deu o erro acima

Tive que iniciar o Xcode para ele gerar algumas configurações
Em seguida tive que instalar o cocoapods :
sudo gem install cocoapods
recebi o aviso que nao tinha uma lib dependente entao instalei a lib
sudo gem install activesupport -v 6.1.7.3
em seguida instale o cocoapods rodando novamente o comando:
sudo gem install cocoapods
Rodei o kdoctor novamente e tudo certo!

Tive que editar o meu profile
vim ~/.zprofile

Segundo Passo: Criar um projeto Multi Plataforma
Crie um novo projeto a partir do seguinte template
- Selecione File | New | New Project.
- Selecione Kotlin Multiplatform App e click em Next.

Digite o nome do projeto e click em Next.

Em iOS framework distribution , selecione a opção Regular framework

Click em Finish.
É recomendado usar a opção Regular framework para um primeiro projeto, pois esta opção não requer ferramentas de terceiros e tem menos problemas de instalação.
Para projetos mais complexos, é necessário o gerenciador de dependências CocoaPods que ajuda a lidar com as dependências da biblioteca. Para saber mais sobre CocoaPods e como configurar um ambiente para eles, consulte Visão geral e configuração do CocoaPods.
Examinando a estrutura do projeto Multi Plataforma
Selecione a visualização Projeto

Basicamente um projeto Kotlin Multi Platforma
possui 3 módulos :

- shared é um módulo Kotlin que contém a lógica comum para aplicativos Android e iOS — o código compartilhado entre as plataformas. Ele usa o Gradle como o sistema de compilação que ajuda a automatizar o processo de compilação. O módulo shared se baseia em uma biblioteca Android e uma estrutura iOS.
- androidApp é um módulo Kotlin integrado a um aplicativo Android. Ele usa o Gradle como sistema de compilação. O módulo androidApp depende e usa o módulo compartilhado como uma biblioteca regular do Android.
- iosApp é um projeto Xcode integrado a um aplicativo iOS. Depende e usa o módulo compartilhado como uma estrutura do iOS. O módulo compartilhado pode ser usado como uma estrutura regular ou como uma dependência do CocoaPods.
O módulo shared consiste em três conjuntos de origem: androidMain, commonMain e iosMain. O conjunto de origem é um conceito do Gradle para vários arquivos agrupados logicamente, onde cada grupo tem suas próprias dependências. No Kotlin Multiplatform, diferentes conjuntos de fontes em um módulo compartilhado podem ser direcionados a diferentes plataformas.
Executar o projeto no simulador iOS
Inicie o Xcode em uma janela separada. Na primeira vez que o Xcode roda, pode ser necessário aceitar os termos de licença e permitir que ele execute algumas tarefas iniciais necessárias.
No Android Studio, selecione iosApp na lista de configurações de execução e clique em Executar.


Se não tiver uma configuração iOS disponível na lista, adicione um novo dispositivo iOS simulado.

Ao selecionar o RUN o Build falhou com a seguinte mensagem de erro:
The following build commands failed:
PhaseScriptExecution Run\ Script /Users/santosjoao/Documents/KotlimMultPlataform/MyMultiPlataformProject/build/ios/iosApp.build/Debug-iphonesimulator/iosApp.build/Script-7555FFB5242A651A00829871.sh (in target 'iosApp' from project 'iosApp')
(1 failure)
Foi necessário atualizar o projeto para rodar com o java 18 e a partir dai a build ocorreu normalmente


Atualizando o Código
- Em shared/src/commonMain/kotlin, abra o arquivo Greeting.kt na pasta do projeto. Este diretório armazena o código compartilhado para Android e iOS. Se fizer alterações no código compartilhado, elas serão refletidas em ambos os aplicativos.
- Atualize o código compartilhado usando reversed(), a função da biblioteca padrão Kotlin para inverter texto que funciona em todas as plataformas:
Terceiro passo: Adicionar dependências ao projeto
Agora vamos aprender como adicionar dependências a bibliotecas de terceiros, o que é necessário para criar aplicativos multiplataforma.
- Tipos de dependência
Existem dois tipos de dependências em projetos Multiplatforma : - Dependências multiplataforma. São bibliotecas multiplataforma que suportam vários destinos e podem ser usadas no conjunto de origem comum, commonMain.
Muitas bibliotecas Android modernas já têm suporte multiplataforma, como Koin, Apollo e Okio. - Dependências nativas. São bibliotecas regulares de ecossistemas relevantes. Em projetos iOS nativos utiliza se o CocoaPods ou outro gerenciador de dependências e em projetos Android o Gradle.
Ao trabalhar com um módulo compartilhado, você também pode depender de dependências nativas e usá-las nos conjuntos de origem nativa, androidMain e iosMain. Normalmente, precisaremos dessas dependências para trabalhar com APIs de plataforma, por exemplo, armazenamento de segurança, e houver lógica comum.
Para ambos os tipos de dependências, pode usar repositórios locais e externos.
Adicionar uma dependência multiplataforma
Adicionar uma dependência multiplataforma é semelhante a adicionar uma dependência Gradle em um projeto Android normal. A única diferença é que precisamos especificar o conjunto de origem.
Vamos agora adicionar a biblioteca kotlinx-datetime, que tem suporte multiplataforma completo.
Navegue até o arquivo build.gradle.kts no diretório compartilhado.
Adicione a seguinte dependência às dependências do conjunto de origem commonMain:
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")

Sincronize os arquivos Gradle clicando em Sincronizar

Em shared/src/commonMain/kotlin, crie um novo arquivo NewYear.kt na pasta do projeto.

Atualize o arquivo com a função que calcula o número de dias de hoje até o Ano Novo usando a lib adicionada
import kotlinx.datetime.*
fun daysUntilNewYear(): Int {
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
val closestNewYear = LocalDate(today.year + 1, 1, 1)
return today.daysUntil(closestNewYear)
}
Na classe Greeting.kt, atualize a função greet() para ver o resultado:
class Greeting {
private val platform: Platform = getPlatform()
fun greet(): String {
return "Olá Mundo do > ${platform.name}!" +
"\nRestam apenas ${daysUntilNewYear()} dias para a chegada de um ano novo!"
}
}
Execute novamente o run no Android Studio:

Quarto passo: Atualizar o projeto
A lógica comum foi implementada usando dependências externas. Agora podemos adicionar uma lógica mais complexa. Solicitações de rede e serialização de dados são os casos mais populares para compartilhar com o Kotlin Multiplatform.
O aplicativo atualizado recuperará dados pela API da SpaceX e exibirá a data do último lançamento bem-sucedido de um foguete da SpaceX.
Adicionar mais dependências
Serão necessárias as seguintes bibliotecas multiplataforma no projeto
- kotlinx.coroutines: para usar corrotinas para escrever código assíncrono,
que permite operações simultâneas. - kotlinx.serialization: para desserializar respostas JSON em
objetos de classes de entidade usadas para processar operações de rede. - Ktor: uma estrutura como cliente HTTP para recuperação de dados pela Internet.
kotlinx.coroutines
Para adicionar kotlinx.coroutines ao seu projeto, especifique uma dependência no conjunto de origem comum. Para fazer isso, adicione a seguinte linha ao arquivo build.gradle.kts do módulo compartilhado:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")gradle

O plug-in Gradle multiplataforma adiciona automaticamente uma dependência às partes específicas da plataforma (iOS e Android) do kotlinx.coroutines.
Se seu projeto usa Kotlin antes da versão 1.7.20 ou o Kotlin 1.7.20 e posterior, já tem o novo gerenciador de memória Kotlin/nativo ativado por padrão. Se não for o caso, adicione o seguinte ao final do arquivo build.gradle.kts:
kotlin.targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java) {
binaries.all {
binaryOptions["memoryModel"] = "experimental"
}
}
kotlinx.serialization
Para kotlinx.serialization, precisamos do plug-in exigido pelo sistema de compilação. O plug-in de serialização Kotlin é fornecido com a distribuição do compilador Kotlin e o plug-in IntelliJ IDEA é incluído no plug-in Kotlin.
Para configurar o plug-in de serialização com o plug-in Kotlin usamos a DSL de plug-ins do Gradle adicionando esta linha ao bloco de plug-ins existente bem no início do arquivo
kotlin("plugin.serialization") version "1.8.20"

Ktor
Podemos adicionar o Ktor da mesma forma que fizemos com a biblioteca kotlinx.coroutines. Além de especificar a dependência principal (ktor-client-core) no conjunto de origem comum, também precisamos:
- Adicionar a funcionalidade ContentNegotiation (ktor-client-content-negotiation), responsável por serializar/desserializar o conteúdo em um formato específico.
- Adicionar a dependência ktor-serialization-kotlinx-json para instruir o Ktor a usar o formato JSON e kotlinx.serialization como uma biblioteca de serialização. Ktor esperará dados JSON e os desserializará em uma classe de dados ao receber respostas.
- Forneçer os mecanismos de plataforma adicionando dependências nos artefatos correspondentes nos conjuntos de origem da plataforma (ktor-client-android, ktor-client-darwin).
val ktorVersion = "2.2.4"
sourceSets {
val commonMain by getting {
dependencies {
// ...
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negociation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:$ktorVersion")
}
}
val iosMain by creating {
// ...
dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}
}
Sincronize os arquivos Gradle clicando em Sincronizar.
Criar requests para a API
Vamos precisar da API SpaceX para obter a lista de todos os lançamentos do endpoint v4/launches.
Adicione um Model dos dados
Em shared/src/commonMain/kotlin, crie um novo arquivo RocketLaunch.kt na pasta do projeto e adicione uma classe de dados que armazena dados da API SpaceX:
package com.example.mymultiplataformproject
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class RocketLaunch (
@SerialName("flight_number")
val flightNumber: Int,
@SerialName("name")
val missionName: String,
@SerialName("date_utc")
val launchDateUTC: String,
@SerialName("success")
val launchSuccess: Boolean?,
)
- A classe RocketLaunch é marcada com a anotação @Serializable, para que o plug-in kotlinx.serialization possa gerar automaticamente um serializador padrão para ela.
- A anotação @SerialName permite redefinir nomes de campos, tornando possível declarar propriedades em classes de dados com nomes mais legíveis.
Conectando HTTP client
Na classeGreeting.kt
, crie uma instancia HTTPClient
para executar chamadas de rede para a api e fazer o parse do JSON
class Greeting {
private val platform: Platform = getPlatform()
fun greet(): String {
return "Olá Mundo do > ${platform.name}!" +
"\nRestam apenas ${daysUntilNewYear()} dias para a chegada de um ano novo!"
}
private val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
}
Para desserializar o resultado da solicitação GET, são usados o plug-in ContentNegotiation Ktor e o serializador JSON.
Na função greet()
, obtenha as informações sobre lançamentos de foguetes chamando o método httpClient.get()
e localize o lançamento mais recente:
suspend fun greet(): String {
val rockets: List<RocketLaunch> =
httpClient.get("https://api.spacexdata.com/v4/launches").body()
val lastSuccessLaunch = rockets.last { it.launchSuccess == true }
return "Olá Mundo do > ${platform.name}!" +
"\nRestam apenas ${daysUntilNewYear()} dias para a chegada de um ano novo!" +
"\n O Ultimo lancamento de foguetes foi em ${lastSuccessLaunch.launchDateUTC}"
}
O modificador suspend
na função greet()
é necessário porque agora contém uma chamada paraget()
. É uma função de suspensão que possui uma operação assíncrona para recuperar dados pela Internet e só pode ser chamada de dentro de uma co-rotina ou outra função de suspensão. A solicitação de rede será executada no pool de encadeamentos do cliente HTTP.
Adicionando permissão de Internet para as chamadas
Para acessar a internet, o aplicativo Android precisa de permissão apropriada. Como todas as solicitações de rede são feitas a partir do módulo compartilhado, faz sentido adicionar a permissão de acesso à Internet ao seu manifesto.
Atualize seu arquivo androidApp/src/main/AndroidManifest.xml
com a permissão de acesso:
<uses-permission android:name="android.permission.INTERNET"/>
Atualizar os projetos Android e iOS
Agora será necessário atualizar as partes nativas (iOS, Android) do projeto, para que possam lidar adequadamente com o resultado da chamada da função greet()
.
Android
Como o módulo compartilhado e o aplicativo Android são escritos em Kotlin, usar o código compartilhado do Android é simples
Em androidApp/src/main/java
, localize o arquivo MainActivity.kt
e atualize a seguinte classe substituindo a implementação anterior:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
var text by remember { mutableStateOf("Loading...") }
LaunchedEffect(true) {
text = try {
Greeting().greet()
} catch (e: Exception) {
e.localizedMessage ?: "error"
}
}
GreetingView(text)
}
}
}
}
}
A função greet()
agora é chamada dentro de LaunchedEffect
para evitar recuperá-la a cada recomposição.
iOS
Para a parte iOS do projeto, você usará SwiftUI para construir a interface do usuário e o padrão Model–view–viewmodel para conectar a UI ao módulo compartilhado, que contém toda a lógica de negócios.
O módulo já está conectado ao projeto iOS — o plugin do Android Studio fez toda a configuração. O módulo já foi importado e usado em ContentView.swift
com importação compartilhada
Inicie o Xcode e selecione Abrir um projeto ou arquivo.
Navegue até seu projeto, nesse exemplo MyMultiPlataformProject, e selecione a pasta iosAppfolder. Clique em Abrir.

Em iosApp/iOSApp.swift
, atualize o ponto de entrada do seu aplicativo:
import SwiftUI
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: ContentView.ViewModel())
}
}
}
Em iosApp/ContentView.swift
, crie uma classe ViewModel
para ContentView
, que preparará e gerenciará dados para ela:
struct ContentView: View {
@ObservedObject private(set) var viewModel: ViewModel
var body: some View {
Text(viewModel.text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension ContentView {
class ViewModel: ObservableObject {
@Published var text = "Loading..."
init() {
Greeting().greet() { greeting, error in
DispatchQueue.main.async {
if let greeting = greeting {
self.text = greeting
} else {
self.text = error?.localizedDescription ?? "error"
}
}
}
}
}
}
ViewModel
é declarada como uma extensão paraContentView
, pois eles estão conectados.- A estrutura Combine conecta o modelo de exibição (
ContentView.ViewModel
) com a exibição (ContentView
). ContentView.ViewModel
é declarada como um objeto observavelObservableObject
.- O wrapper
@Published
é usado para a propriedade de texto. - O wrapper
@ObservedObject
é usado para se inscrever no observador da viewModel - Agora, a viewModelemitirá sinais sempre que essa propriedade for alterada.
Chame a função greet(), que agora também carrega dados da API SpaceX, e salve o resultado na propriedade text:
Rebuild o projeto no Xcode que o erro some
Extra trailing closure passed in call
Rebuild o projeto no Xcode que o erro some ou execute o iosApp do Android Studio.
Rode novamente o projeto pelo Android Studio para ver as alterações

Quinto passo: Finalizar o projeto
Conclusões
O Kotlin multi plataforma ajuda a compartilhar os módulos data e domain do aplicativo entre plataformas Android, iOS . Isso significa que um desenvolvedor Android com experiência em Kotlin pode reutilizar seu código no iOS sem a necessidade de uma estrutura ou linguagem de terceiros reduzindo significativamente o custo e o tempo de desenvolvimento para criar aplicativos móveis.
Antes, havia uma desvantagem: o Kotlin Multiplatforms não suportava o compartilhamento da UI entre as plataformas, porém, isso mudou, com o lançamento do JetPack Compose Multiplatform, uma estrutura declarativa para compartilhar UIs entre várias plataformas com Kotlin.
Agora é possível escolher as plataformas nas quais compartilhar as UIs usando o Compose Multiplatform:
- iOS (Alfa)
- Android (via Jetpack Compose)
- Área de trabalho (Windows, MacOS, Linux)
- Web (Experimental)
Isso agora compete de frente com o Flutter!! O Futuro começou!!
