Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Idiomaschevron-right
Kotlinchevron-right

Dominando o Kotlin: criando uma API com Ktor e MongoDB Atlas

Ricardo Mello9 min read • Published Sep 17, 2024 • Updated Sep 17, 2024
Kotlin
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Asimplicidade do Kotlin, a interoperabilidade Java e a estrutura fácil de usar do Ktor, combinadas com o banco de dados em nuvem flexível do MongoDB Atlas, fornecem uma pilha robusta para o desenvolvimentode software moderno.
Juntos, demonstraremos e configuraremos o projeto Ktor, implementaremos operações CRUD, definiremos pontos de conexão de rota de API e executaremos o aplicativo. Ao final, você terá uma sólida compreensão dos recursos do Kotlin no desenvolvimento de API e das ferramentas necessárias para ter sucesso no desenvolvimento de software moderno.

Demonstração

Demonstração do projeto Kotlin e Ktor com MongoDB
Como você pode ver acima, nosso aplicativo será capaz de executar as operações descritas na imagem. Para fazer isso, utilizaremos um modelo de dados estruturado semelhante ao exemplo fornecido:
1fitness {
2 _id: objectId,
3 exerciseType: String,
4 notes: String,
5 fitnessDetails: {
6 durationMinutes: Int,
7 distance: Double,
8 caloriesBurned: Int
9 }
10}

Configurando o projeto Ktor

O Ktor é uma estrutura web assíncrona e baseada em Kotlin, projetada para criar aplicativos web e APIs modernos. Desenvolvido pela JetBrains, a mesma equipe por trás do Kotlin, o Ktor simplifica o processo de criação de aplicativos da web e, ao mesmo tempo, oferece suporte robusto para programação assíncrona usando corrotinas do Kotlin.
Para iniciar a configuração do projeto, usaremos o gerador de projetosKtor.
Gerador de projeto Ktor - Ajustando as configurações do projeto
Conforme representado nas imagens, precisamos configurar parâmetros como nome do projeto, arquivo de configuração etc.
Na seção subsequente, especificamos os seguintes plug-ins:
  • Negociação de conteúdo: facilita a negociação de tipos de mídia entre o cliente e o servidor
  • GSON: oferece recursos de serialização e desserialização
  • Roteamento: gerencia solicitações recebidas em um aplicativo de servidor
  • Swagger: simplifica a documentação e o desenvolvimento da API
Ktor Project Generator - Adicionando plugins
Depois que todas as configurações estiverem em vigor, basta clicar em "Generate Project " para continuar com a criação de nosso projeto.
Após importar o projeto, vamos abrir o arquivobuild.gradle.kts para incorporar dependências adicionais. Especificamente, adicionaremos dependências para o driver Kotlin do lado do servidor MongoDB e Koin para dependência de injeção.
1dependencies {
2 implementation("io.ktor:ktor-server-core-jvm")
3 implementation("io.ktor:ktor-server-swagger-jvm")
4 implementation("io.ktor:ktor-server-content-negotiation-jvm")
5 implementation("io.ktor:ktor-serialization-gson-jvm")
6 implementation("io.ktor:ktor-server-tomcat-jvm")
7 implementation("ch.qos.logback:logback-classic:$logback_version")
8
9 //MongoDB
10 implementation("org.mongodb:mongodb-driver-kotlin-coroutine:4.10.1")
11
12 //Koin Dependency Injection
13 implementation("io.insert-koin:koin-ktor:3.5.3")
14 implementation("io.insert-koin:koin-logger-slf4j:3.5.3")
15}

Implementando operações CRUD

Antes de criar nosso projeto, vamos organiza-lo em vários pacotes. Para isso, criaremos três pacotes: application, domaine infrastructure como mostrado na imagem abaixo:
Estrutura do projeto
Agora, vamos criar um pacote chamado entity dentro do domínio e incluir um arquivo chamado Fitness.kt:
domain/entity/Fitness.kt
1package com.mongodb.domain.entity
2import com.mongodb.application.response.FitnessResponse
3import org.bson.codecs.pojo.annotations.BsonId
4import org.bson.types.ObjectId
5
6data class Fitness(
7 @BsonId
8 val id: ObjectId,
9 val exerciseType: String,
10 val notes: String,
11 val details: FitnessDetails
12){
13 fun toResponse() = FitnessResponse(
14 id = id.toString(),
15 exerciseType = exerciseType,
16 notes = notes,
17 details = details
18 )
19}
20
21data class FitnessDetails(
22 val durationMinutes: Int,
23 val distance: Double,
24 val caloriesBurned: Int
25)
Como você pode observar, nossa classe tem um erro no toResponse() método porque ainda não criamos a FitnessResponse classe . Para corrigir esse problema, precisamos criar a FitnessResponse classe . Vamos aproveitar esta oportunidade para criar FitnessResponse e FitnessRequest. Essas classes manipularão os dados trocados em solicitações HTTP relacionadas à entidade Fitness. Dentro do pacote do aplicativo,request response crie os pacotes e . Inclua as FitnessRequest FitnessResponse classes e em cada uma, respectivamente.
application/request/FitnessRequest.kt:
1package com.mongodb.application.request
2
3import com.mongodb.domain.entity.Fitness
4import com.mongodb.domain.entity.FitnessDetails
5import org.bson.types.ObjectId
6
7data class FitnessRequest(
8 val exerciseType: String,
9 val notes: String,
10 val details: FitnessDetails
11)
12fun FitnessRequest.toDomain(): Fitness {
13 return Fitness(
14 id = ObjectId(),
15 exerciseType = exerciseType,
16 notes = notes,
17 details = details
18 )
19}
application/response/FitnessResponse.kt:
1package com.mongodb.application.response
2
3
4import com.mongodb.domain.entity.FitnessDetails
5
6
7data class FitnessResponse(
8 val id: String,
9 val exerciseType: String,
10 val notes: String,
11 val details: FitnessDetails
12)
Se tudo estiver correto, nossa estrutura ficará semelhante à imagem abaixo:
Estrutura do projeto
Agora, é hora de criar nossa interface que se comunicará com nosso banco de dados. Para fazer isso, dentro do pacotedomain, criaremos outro pacote chamado portse, posteriormente, a interfaceFitnessRepository .
domain/ports/FitnessRepository:
1package com.mongodb.domain.ports
2
3import com.mongodb.domain.entity.Fitness
4import org.bson.BsonValue
5import org.bson.types.ObjectId
6
7interface FitnessRepository {
8 suspend fun insertOne(fitness: Fitness): BsonValue?
9 suspend fun deleteById(objectId: ObjectId): Long
10 suspend fun findById(objectId: ObjectId): Fitness?
11 suspend fun updateOne(objectId: ObjectId, fitness: Fitness): Long
12}
Ideal! Agora, precisamos implementar os métodos de nossa interface. Acessaremos o pacoteinfrastructure e criaremos um pacoterepository dentro dele.Em seguida, crie uma classeFitnessRepositoryImple implemente os métodos conforme mostrado no código abaixo:
infrastructure/repository/FitnessRepositoryImpl
1package com.mongodb.infrastructure.repository
2
3import com.mongodb.MongoException
4import com.mongodb.client.model.Filters
5import com.mongodb.client.model.UpdateOptions
6import com.mongodb.client.model.Updates
7import com.mongodb.domain.entity.Fitness
8import com.mongodb.domain.ports.FitnessRepository
9import com.mongodb.kotlin.client.coroutine.MongoDatabase
10import kotlinx.coroutines.flow.firstOrNull
11import org.bson.BsonValue
12import org.bson.types.ObjectId
13
14class FitnessRepositoryImpl(
15 private val mongoDatabase: MongoDatabase
16) : FitnessRepository {
17
18 companion object {
19 const val FITNESS_COLLECTION = "fitness"
20 }
21
22 override suspend fun insertOne(fitness: Fitness): BsonValue? {
23 try {
24 val result = mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).insertOne(
25 fitness
26 )
27 return result.insertedId
28 } catch (e: MongoException) {
29 System.err.println("Unable to insert due to an error: $e")
30 }
31 return null
32 }
33
34 override suspend fun deleteById(objectId: ObjectId): Long {
35 try {
36 val result = mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).deleteOne(Filters.eq("_id", objectId))
37 return result.deletedCount
38 } catch (e: MongoException) {
39 System.err.println("Unable to delete due to an error: $e")
40 }
41 return 0
42 }
43
44 override suspend fun findById(objectId: ObjectId): Fitness? =
45 mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).withDocumentClass<Fitness>()
46 .find(Filters.eq("_id", objectId))
47 .firstOrNull()
48
49 override suspend fun updateOne(objectId: ObjectId, fitness: Fitness): Long {
50 try {
51 val query = Filters.eq("_id", objectId)
52 val updates = Updates.combine(
53 Updates.set(Fitness::exerciseType.name, fitness.exerciseType),
54 Updates.set(Fitness::notes.name, fitness.notes),
55 Updates.set(Fitness::details.name, fitness.details)
56 )
57 val options = UpdateOptions().upsert(true)
58 val result =
59 mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION)
60 .updateOne(query, updates, options)
61
62 return result.modifiedCount
63 } catch (e: MongoException) {
64 System.err.println("Unable to update due to an error: $e")
65 }
66 return 0
67 }
68}
Conforme observado, a classe implementa a interfaceFitnessRepositorye inclui um objeto complementar. Ele recebe uma instânciaMongoDatabasecomo parâmetro do construtor. O objeto complementar define uma constante chamada FITNESS_COLLECTION, com um valor de "fitness, " indicando o nome da collection do MongoDB em que operações como inserir, localizar, excluir e atualizar são realizadas em dados relacionados à aptidão.
Se tudo estiver correto, nossa estrutura será a seguinte:
Estrutura do projeto

Desenvolvendo pontos de extremidade da API

Para criar nossos endpoints, é fundamentalcompreender oroteamento. No Ktor, o roteamento determina como o servidor lida com as solicitações de entrada para URLs específicos, permitindo que os desenvolvedores definam ações ou respostas para cada endpoint. Antes de prosseguir com o processo de criação, vamos revisar brevemente os endpoints que estaremos construindo:
  1. GET /Fitness/{id}: utilize este endpoint para obter informações detalhadas sobre uma atividade de preparação física específica com base em seu identificador exclusivo (ID).
  2. POST /fitness: com esse endpoint, você pode adicionar facilmente novas atividades de condicionamento físico ao seu monitor, garantindo que todos os dados do seu treinoestejam atualizados.
  3. PATCH /Fitness/{ID}: precisa atualizar ou modificar uma atividade de preparação física existente? Esse endpoint permite que você faça alterações direcionadas a atividades específicas identificadas por seu ID exclusivo.
  4. DELETE /fitness/{id}: por fim, esse endpoint permite que você remova atividades de condicionamento físico indesejadas ou desatualizadas do seu rastreador, mantendo um registro limpo e preciso de sua jornada de condicionamento físico.
Agora que sabemos os métodos que implementaremos, vamos criar a classe que fará esse trabalho. Dentro do pacoteapplication, vamos criar um novo chamado routes e o arquivo abaixo com o nome FitnessRoute.kt com o conteúdo abaixo:
application/routes/FitnessRoutes.kt
1package com.mongodb.application.routes
2
3import com.mongodb.application.request.FitnessRequest
4import com.mongodb.application.request.toDomain
5import com.mongodb.domain.ports.FitnessRepository
6import io.ktor.http.HttpStatusCode
7import io.ktor.server.application.call
8import io.ktor.server.request.receive
9import io.ktor.server.response.respond
10import io.ktor.server.response.respondText
11import io.ktor.server.routing.route
12import io.ktor.server.routing.Route
13import io.ktor.server.routing.post
14import io.ktor.server.routing.delete
15import io.ktor.server.routing.get
16import io.ktor.server.routing.patch
17import org.bson.types.ObjectId
18import org.koin.ktor.ext.inject
19
20fun Route.fitnessRoutes() {
21 val repository by inject<FitnessRepository>()
22 route("/fitness") {
23 post {
24 val fitness = call.receive<FitnessRequest>()
25 val insertedId = repository.insertOne(fitness.toDomain())
26 call.respond(HttpStatusCode.Created, "Created fitness with id $insertedId")
27 }
28
29 delete("/{id?}") {
30 val id = call.parameters["id"] ?: return@delete call.respondText(
31 text = "Missing fitness id",
32 status = HttpStatusCode.BadRequest
33 )
34 val delete: Long = repository.deleteById(ObjectId(id))
35 if (delete == 1L) {
36 return@delete call.respondText("Fitness Deleted successfully", status = HttpStatusCode.OK)
37 }
38 return@delete call.respondText("Fitness not found", status = HttpStatusCode.NotFound)
39 }
40
41 get("/{id?}") {
42 val id = call.parameters["id"]
43 if (id.isNullOrEmpty()) {
44 return@get call.respondText(
45 text = "Missing id",
46 status = HttpStatusCode.BadRequest
47 )
48 }
49 repository.findById(ObjectId(id))?.let {
50 call.respond(it.toResponse())
51 } ?: call.respondText("No records found for id $id")
52 }
53
54 patch("/{id?}") {
55 val id = call.parameters["id"] ?: return@patch call.respondText(
56 text = "Missing fitness id",
57 status = HttpStatusCode.BadRequest
58 )
59 val updated = repository.updateOne(ObjectId(id), call.receive())
60 call.respondText(
61 text = if (updated == 1L) "Fitness updated successfully" else "Fitness not found",
62 status = if (updated == 1L) HttpStatusCode.OK else HttpStatusCode.NotFound
63 )
64 }
65 }
66}
Como você pode observar, configuramos chamadas de método e utilizamos call para definir ações nos manipuladores de rotas. O Ktor permite gerenciar solicitações recebidas e enviar respostas diretamente dentro desses manipuladores, oferecendo flexibilidade no tratamento de diferentes cenários de solicitação. Além disso, estamos utilizando o Koin para injeção de dependência no FitnessRepository repositório .
Agora, antes de executarmos o aplicativo, precisamos instalar a dependência Koin e incorporá-la como um módulo.
Os módulos no Ktor são usados para organizar e encapsular a funcionalidade em seu aplicativo. Eles permitem agrupar rotas, dependências e configurações relacionadas para facilitar o gerenciamento e a manutenção.
Para isso, abra a classeApplication.kte substitua todo o seu código pelo código abaixo:
com.mongodb.Application.kt
1package com.mongodb
2
3import com.mongodb.application.routes.fitnessRoutes
4import com.mongodb.domain.ports.FitnessRepository
5import com.mongodb.infrastructure.repository.FitnessRepositoryImpl
6import com.mongodb.kotlin.client.coroutine.MongoClient
7import io.ktor.serialization.gson.gson
8import io.ktor.server.application.Application
9import io.ktor.server.application.install
10import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
11import io.ktor.server.plugins.swagger.swaggerUI
12import io.ktor.server.routing.routing
13import io.ktor.server.tomcat.EngineMain
14import org.koin.dsl.module
15import org.koin.ktor.plugin.Koin
16import org.koin.logger.slf4jLogger
17
18fun main(args: Array<String>): Unit = EngineMain.main(args)
19
20fun Application.module() {
21 install(ContentNegotiation) {
22 gson {
23 }
24 }
25 install(Koin) {
26 slf4jLogger()
27 modules(module {
28 single { MongoClient.create(
29 environment.config.propertyOrNull("ktor.mongo.uri")?.getString() ?: throw RuntimeException("Failed to access MongoDB URI.")
30 ) }
31 single { get<MongoClient>().getDatabase(environment.config.property("ktor.mongo.database").getString()) }
32 }, module {
33 single<FitnessRepository> { FitnessRepositoryImpl(get()) }
34 })
35 }
36 routing {
37 swaggerUI(path = "swagger-ui", swaggerFile = "openapi/documentation.yaml") {
38 version = "4.15.5"
39 }
40 fitnessRoutes()
41 }
42}
Vamos dividir o que esse código está fazendo em três seções principais para facilitar a compreensão:
  1. Configuração do ContentNegotiation: estamos configurando o ContentNegotiation para lidar com a serialização e a desserialização JSON. Especificamente, estamos usando o Gson como formatador JSON padrão, garantindo uma comunicação perfeita entre nosso aplicativo e os clientes.
  2. Injeção de dependência com Koin**:** Na seção subsequente, estamos integrando Koin para gerenciamento de injeção de dependência. No primeiro módulo, estabelecemos a conexão com o MongoDB, recuperando o URI e o nome do banco de dados do arquivo configuration.conf. Posteriormente, definimos injeção para o FitnessRepository.
  3. Configuração de roteamento: Durante a fase de roteamento, configuramos a rota da API Swagger, acessível em /swagger-ui. Além disso, especificamos nossa rota para o tratamento de operações relacionadas ao Fitness.
Ótimo. Agora precisamos fazer os ajustes finais antes de executar nossa aplicação. Primeiro, abra o arquivoapplication.conf e inclua as seguintes informações:
recursos/application.conf
1ktor {
2 deployment {
3 port = 8080
4 }
5 application {
6 modules = [ com.mongodb.ApplicationKt.module ]
7 }
8 mongo {
9 uri = ${?MONGO_URI}
10 database = ${?MONGO_DATABASE}
11 }
12}
Aviso: Essas variáveis serão utilizadas no final do artigo quando executarmos a aplicação.
Muito bem. Agora, basta abrir o arquivodocumentation.yaml e substituí-lo pelo conteúdo abaixo. Neste arquivo, estamos indicando quais métodos nossa API fornecerá quando acessada por meio /swagger-ui:
recursos/openapi/documentation.yaml
1openapi: 3.0.0
2info:
3 title: Fitness API
4 version: 1.0.0
5 description: |
6 This Swagger documentation file outlines the API specifications for a Fitness Tracker application built with Ktor and MongoDB. The API allows users to manage fitness records including creating new records, updating and deleting records by ID. The API uses the Fitness and FitnessDetails data classes to structure the fitness-related information.
7
8paths:
9 /fitness:
10 post:
11 summary: Create a new fitness record
12 requestBody:
13 required: true
14 content:
15 application/json:
16 schema:
17 $ref: '#/components/schemas/FitnessRequest'
18 responses:
19 '201':
20 description: Fitness created successfully
21 '400':
22 description: Bad request
23 /fitness/{id}:
24 get:
25 summary: Retrieve fitness record by ID
26 parameters:
27 - name: id
28 in: path
29 required: true
30 schema:
31 type: string
32 responses:
33 '200':
34 description: Successful response
35 content:
36 application/json:
37 example: {}
38 '404':
39 description: Fitness not found
40 delete:
41 summary: Delete fitness record by ID
42 parameters:
43 - name: id
44 in: path
45 required: true
46 schema:
47 type: string
48 responses:
49 '200':
50 description: Fitness deleted successfully
51 '400':
52 description: Bad request
53 '404':
54 description: Fitness not found
55 patch:
56 summary: Update fitness record by ID
57 parameters:
58 - name: id
59 in: path
60 required: true
61 schema:
62 type: string
63 requestBody:
64 required: true
65 content:
66 application/json:
67 schema:
68 $ref: '#/components/schemas/FitnessRequest'
69 responses:
70 '200':
71 description: Fitness updated successfully
72 '400':
73 description: Bad request
74 '404':
75 description: Fitness not found
76
77components:
78 schemas:
79 Fitness:
80 type: object
81 properties:
82 id:
83 type: string
84 format: uuid
85 exerciseType:
86 type: string
87 notes:
88 type: string
89 details:
90 $ref: '#/components/schemas/FitnessDetails'
91 required:
92 - id
93 - notes
94 - details
95
96 FitnessDetails:
97 type: object
98 properties:
99 durationMinutes:
100 type: integer
101 format: int32
102 distance:
103 type: number
104 format: double
105 caloriesBurned:
106 type: integer
107 format: int32
108 required:
109 - durationMinutes
110 - distance
111 - caloriesBurned
112
113 FitnessRequest:
114 type: object
115 properties:
116 exerciseType:
117 type: string
118 notes:
119 type: string
120 details:
121 $ref: '#/components/schemas/FitnessDetails'
122 required:
123 - exerciseType
124 - notes
125 - details
O ajuste final é excluir a pastaplugins, que contém os arquivosHTTP.kt , Routing.kte Serialization.kt, pois já os incluímos na classeApplication.kt. Além disso, exclua a classeApplicationTest, pois nosso foco aqui não está nos testes. Podemos discutir testes outra hora. Após essas alterações, sua estrutura deverá ficar semelhante a esta:
Estrutura do projeto

Executando o aplicativo

Para executar nosso aplicativo, precisamos de uma connection string do MongoDB Atlas. Se você ainda não tem acesso, crie sua conta.
Depois que sua conta for criada, acesse o menuVisão geral ,depois em Conectar e selecione Kotlin. Depois disso, nossa connection string estará disponível conforme mostrado na imagem abaixo:
String de conexão do cluster do MongoDB Atlas
Com a connection string em mãos, vamos retornar ao IntelliJ, abra a classeApplication.kt e clique no botão executar, conforme mostrado na imagem:
Executando o aplicativo
Nesse estágio, você notará que nosso aplicativo encontrou um erro ao se conectar ao MongoDB Atlas. É aqui que precisamos fornecer o MONGO_URI e MONGO_DATABASE definidos no arquivoapplication.conf:
Para fazer isso, basta editar a configuração e incluir os valores conforme mostrado na imagem abaixo:
1-DMONGO_URI= Add your connection string
2-DMONGO_DATABASE= Add your database name
Clique em “Apply” e execute o aplicativo novamente.
Configurações de execução do IntelliJ
Atenção: lembre-se de alterar a connection string para seu nome de usuário, senha e cluster no MongoDB Atlas.
Agora, basta acessar localhost:8080/swagger-ui e executar as operações.
Painel do Swagger
Abra o método de postagem e insira um objeto para teste:
Respostas do Swagger
Podemos visualizar os dados registrados em nosso cluster MongoDB Atlas como mostrado na imagem abaixo:
Explorador de documentos do MongoDB Atlas

Conclusão

O serviço de API MongoDB Atlas, Ktor e Kotlin juntos forma uma combinação poderosa para criar aplicativos robustos e escaláveis. O MongoDB Atlas fornece uma solução de banco de dados baseada na nuvem que oferece flexibilidade, escalabilidade e confiabilidade, tornando-o ideal para aplicativos modernos. O Ktor, com sua natureza leve e assíncrona, permite o rápido desenvolvimento de serviços web e APIs em Kotlin, aproveitando a sintaxe concisa e os recursos avançados da linguagem. Quando combinados, o MongoDB Atlas e o Ktor em Kotlin permitem que os desenvolvedores criem APIs de alto desempenho com facilidade, fornecendo integração perfeita com bancos de dados MongoDB e mantendo a simplicidade e a flexibilidade no desenvolvimento de aplicativos. Essa combinação capacita os desenvolvedores a criar aplicativos modernos e orientados por dados que podem ser facilmente dimensionados para atender às necessidades de negócios em evolução.
O exemplo de código-fonte usado nesta série está disponível no Github
Alguma dúvida? Venha conversar conosco no MongoDB Developer Community.
Principais comentários nos fóruns
Avatar do Comentarista do Fórum
Lucas_Carrijo_FerrariLucas Carrijo Ferrarilast quarter

Muito obrigado pelo artigo. Achei muito mais claro sua explicação de como conectar o Mongo, mais claro que a própria documentação do Ktor explicando como se conecta no Postgresql.

Só um comentário, o Ktor recomenda, além do GSON que você utilizou, o KotlinX para serialização, a implementação é da mesma forma?

Pelo nome já percebi que era brasileiro (tambem fui no Linkedin pra ter certeza haha).

Veja mais nos fóruns

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Além do básico: aprimorando a API Kotlin Ktor com pesquisa vetorial


Sep 18, 2024 | 9 min read
Tutorial

Começando com Kotlin e MongoDB no lado do servidor


Oct 08, 2024 | 6 min read
exemplo de código

Construindo Splash Screen Nativamente, Android 12, Kotlin


May 12, 2022 | 3 min read
Notícias e Anúncios

Introdução ao Atlas Device Sync para Android


Oct 19, 2022 | 4 min read
Sumário