KMP 101: Explorando as dependências internas e externas no KMP (fim da série)
Rodrigo Sicarelli
Posted on January 27, 2024
Nos artigos anteriores, estabelecemos uma base sobre o Kotlin Multiplatform (KMP) e como ele compila para múltiplas plataformas.
Neste artigo, vamos explorar o uso de bibliotecas open-source, compreender sua aplicabilidade em nossos projetos e, por fim, sua implementação.
Depêndencias e os Source Sets
Descobrimos que o Kotlin utiliza uma estrutura de source sets para gerenciar as compilações distintas.
Cada source set no Kotlin, seja commonMain
ou específicos como androidMain
, native/ios
, desktop
, js
, pode declarar dependências usadas exclusivamente nesse contexto.
Exemplo:
commonMain.dependencies {
// compartilhado por todos os source sets
}
androidMain.dependencies {
// common + Android
}
appleMain.dependencies {
// common + família Apple
}
iosMain.dependencies {
// common + apple + iOS
}
Source Set é um ambiente único
Cada source set do Kotlin se torna um ambiente isolado, com acesso a APIs e SDKs específicos da plataforma.
Por exemplo, no source set do Android, você tem acesso ao Android SDK; no iOS, ao DarwinOS e ao SDK da Apple como platform.UiKit
e componentes do platform.Foundation
.
Implementamos abaixo um Logger em KMP de forma totalmente nativa, sem dependências externas, usando apenas os SDKs nativos:
// src/commonMain/Logger.kt
interface Logger {
fun e(message: String, error: Throwable)
}
// src/androidMain/Logger.android.kt
import android.util.Log
class AndroidLogger : Logger {
override fun e(message: String, error: Throwable) {
Log.e("TAG", message)
error.printStackTrace()
}
}
// src/appleMain/Exemplo.apple.kt
import kotlinx.cinterop.ptr
import platform.darwin.OS_LOG_DEFAULT
import platform.darwin.OS_LOG_TYPE_ERROR
import platform.darwin.__dso_handle
import platform.darwin._os_log_internal
class DarwinLogger : Logger {
override fun e(message: String, error: Throwable) {
_os_log_internal(
__dso_handle.ptr,
OS_LOG_DEFAULT,
OS_LOG_TYPE_ERROR,
"%s",
message
)
error.printStackTrace()
}
}
Entendendo como as depêndencias no KMP funcionam
Considere o build.gradle.kts
com o ktor-client aplicado e dependências declaradas. Ao sincronizar o projeto, dependências são incluídas conforme os targets especificados.
kotlin {
androidTarget()
jvm("desktop")
iosX64()
iosArm64()
iosSimulatorArm64()
watchosArm32()
watchosArm64()
watchosSimulatorArm64()
macosArm64()
tvosArm64()
sourceSets {
commonMain.dependencies {
implementation(libs.ktor.client.core)
}
}
}
A imagem a seguir representa apenas uma parte dessas depêndencias:
Ao declarar os targets e importar uma depêndencia no commonMain
todas essas depêndencias são importadas no projeto.
Se removêssemos alguns targets do nosso build.gradle.kts
e sincronizar o projeto, observamos que as depêndencias específicas de cada source set sumiram:
// removidos:
watchosArm32()
watchosArm64()
watchosSimulatorArm64()
macosArm64()
tvosArm64()
Ou seja, cada target declarado espera que uma depêndencia exista, seja ela publicada em algum artefato como Maven, ou depêndencia de um módulo interno.
Relação entre depêndencias externas e os targets do módulo
Para utilizar uma depêndencia um source set, é obrigatório que essa depêndencia exista para o target em específico.
Por exemplo, para você declarar depêndencias no commonMain
, um artefato (interno ou externo) específico para o common main deve existir.
O mesmo se aplica para os outros targets. Por exemplo, se você declara o watchosArm32()
como target, e seu módulo interno ou biblioteca não possuem esses alvos declarados, você recebe um erro.
Dissecando a depêndencia commonMain
A commonMain
funciona de forma singular em relação aos outros Source Sets. No momento da compilação, ela funciona apenas como metadata
, ou seja, não é compilado diretamente em código executável para uma plataforma específica, mas sim em um formato intermediário que contém metadados.
Estes metadados são então usados pelos backends do Kotlin específica para gerar o código executável correspondente para cada plataforma.
Ao explorar o conteúdo dessa depêndencia, notamos uma extensão especial do KMP: a .klib
.
O arquivo .klib
no KMP é uma biblioteca que contém código compartilhável entre diferentes plataformas.
No contexto do commonMain
, o .klib
funciona como uma coleção de código-fonte e recursos que podem ser compilados para várias plataformas utilizando os diferentes backends.
Se expandirmos a pasta linkdata
, vamos nos deparar com outro formato de arquivo especial do KMP: .knm
O formato de arquivo .knm
é um formato binário utilizado internamente pelas bibliotecas klib
do Kotlin/Native, especialmente em conjunto com a ferramenta cinterop
.
Esse formato contém metadados e informações que o compilador do Kotlin usa para compilar e interligar bibliotecas nativas. Os arquivos .knm
são detalhes de implementação para facilitar a interoperabilidade e a criação de bibliotecas no contexto do Kotlin/Native.
O último arquivo é o manifest
. Esse arquivo contém metadados sobre a própria biblioteca. Isso inclui informações como a versão da biblioteca, dependências necessárias, e outros metadados usados pelo sistema de build e pelo compilador para entender como integrar e usar a biblioteca no projeto. Cada .klib
tem um manifesto que descreve seu conteúdo e como ele deve ser tratado durante a compilação e o link de execução.
Dissecando a depêndencia do iOS
Dependendo de quais plataforma Apple você inclui no seu Source Set, uma depêndencia diferente é importada no projeto.
Note que, além dos Source Sets declarados no nosso build.gradle.kts
, também existe a depêndencia posix
.
A dependência "posix" em um contexto de Kotlin Multiplatform para iOS se refere a interfaces de programação de aplicativos para sistemas operacionais compatíveis com POSIX (Portable Operating System Interface),
No caso de iOS, posixMain
indica que essa biblioteca está usando APIs POSIX, comuns em sistemas baseados em Unix, como o iOS.
Explorando arquivos do .klib
do iOS
Ao analisarmos o conteúdo da .klib
de um target iOS, verificamos uma estrutura similar ao commonMain
, porém com uma pasta ir
e outra targets.ios_X
.
A pasta ir
representa diferentes componentes do código e metadados compilados:
-
bodies.knb
: Contém os corpos das funções compiladas. -
debugInfo.knd
: Informações de depuração que permitem o rastreamento de erros e a inspeção do código durante o desenvolvimento e a depuração. -
files.knf
: Lista dos arquivos de origem compilados na biblioteca. -
irDeclarations.knd
: Declarações intermediárias da Representação Intermediária (IR) que o compilador utiliza para compilar o código Kotlin. -
signatures.knt
: Assinaturas das funções e tipos na biblioteca, usadas para identificação única dentro do código compilado. -
strings.knt
: Strings literais usadas no código da biblioteca. -
types.knt
: Informações sobre os tipos usados na biblioteca, como classes, interfaces e tipos primitivos.
A pasta targets.ios_X
não possuí nenhum conteúdo nesse caso. Mas, nessa pasta reside arquivos de "bitcode" LLVM, que contém código intermediário utilizado pelo compilador LLVM.
Dissecando a depêndencia do JS
Para o target JS, ainda temos um arquivo .klib
, mas acompanhado de um package.json
.
Dissecando a depêndencia do Android
No caso do Android e JVM, a depêndencia não é um .klib
, mas sim um .jar
convencional do mundo JVM.
Nesse caso, observamos um formato de .jar
normal de qualquer programa em Java/Kotlin.
Note que essa depêndencia é utilizada tanto pelo Source Set android
quanto ao desktop
:
Como descobrir se uma biblioteca open-source é compatível com meu target?
Para verificar a compatibilidade de uma biblioteca open-source com um target, é recomendável consultar onde a biblioteca está hospedada e quais artefatos estão disponíveis. Você também pode analisar o build.gradle.kts
da biblioteca, e verificar quais targets aquela biblioteca compila.
No caso do ktor-client-core
, ao acessar o Maven Central e pesquisar pelo grupo, encontramos uma lista de artefatos para cada source set.
Depêndencias de módulos internos
Para módulos internos, é essencial que o módulo consumidor tenha targets compatíveis com o módulo consumido.
Vamos supor que o módulo :shared1
quer consumir o módulo :shared2
. Note que o módulo :shared2
possuí os mesmos targets do :shared1
+ o js()
.
Nesse caso, o :shared1
consegue consumir o :shared2
já que o :shared2
compila para o target que o :shared1
precisa.
Agora, o contrário já não é possível: o módulo :shared2
espera um target js()
que o módulo :shared1
não oferece! Nesse caso, há um erro de compilação.
// :shared1 build.gradle.kts
kotlin {
androidTarget()
iosARM64()
}
// :shared2 build.gradle.kts
kotlin {
androidTarget()
iosARM64()
js()
}
Conclusões
Compreender o funcionamento das dependências internas e externas no Kotlin Multiplatform (KMP) é crucial, pois isso nos ajuda a selecionar bibliotecas que atendam às necessidades de nossos projetos.
Neste artigo, exploramos mais profundamente as "entranhas" dessas dependências e como a declaração dos targets em nossa aplicação influencia as dependências incluídas no projeto.
Além disso, aprofundamo-nos nos conceitos de .klib
e .knm
. Embora não afetem nosso dia a dia de desenvolvimento de forma significativa, essas peças são essenciais para entender como o KMP realiza sua "mágica".
Fim da série KMP101!
É com grande satisfação que concluímos esta fundação no KMP!
Espero que o conhecimento adquirido sirva como um trampolim para que você possa explorar e navegar pelo mundo do KMP com confiança.
Fique atento para a série KMP102, onde mergulharemos ainda mais em implementações, arquitetura, testes, interoperabilidade com Swift e outras linguagens, e muito mais!
Um abraço e até a próxima!
🤖 Artigo foi escrito com o auxílio do ChatGPT 4, utilizando o plugin Web.
As fontes e o conteúdo são revisados para garantir a relevância das informações fornecidas, assim como as fontes utilizadas em cada prompt.
No entanto, caso encontre alguma informação incorreta ou acredite que algum crédito está faltando, por favor, entre em contato!
Referências
Discussão sobre o KNM no KotlinLang
Posted on January 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.