Blog para desarrollo de aplicaciones en Android, aprende paso a paso como crear aplicaciones.

Usamos cookies propias y de terceros que entre otras cosas recogen datos sobre sus hábitos de navegación para mostrarle publicidad personalizada y realizar análisis de uso de nuestro sitio.
Si continúa navegando consideramos que acepta su uso. OK Más información | Y más

Retrofit + Kotlinx Serialization en Android

Retrofit + Kotlinx Serialization en Android

Hola amigos 👋 Bienvenidos a un nuevo tutorial de Universo Android. Hoy aprenderemos a usar Retrofit con Kotlinx Serialization, la alternativa moderna a Gson que ofrece mejor rendimiento, type-safety y está optimizada para Kotlin.

Al finalizar este tutorial tendrás:

  • Retrofit con Kotlinx Serialization
  • Modelos con @Serializable
  • Peticiones GET, POST, PUT, DELETE
  • Conversión automática JSON
  • Manejo de nombres personalizados
  • RecyclerView con datos de API
  • Ejemplos prácticos

🟩 1. ¿Qué es Kotlinx Serialization?

Kotlinx Serialization es la biblioteca oficial de JetBrains para serialización en Kotlin. Es más rápida que Gson, type-safe y genera código en compile-time.

🟩 2. Ventajas vs Gson

CaracterísticaKotlinx SerializationGson
VelocidadMás rápidaRápida
Type-safetySíNo
MultiplatformSíNo
Compile-timeSíReflexión
TamañoMenorMayor

🟩 3. Crear el Proyecto

Creamos un nuevo proyecto en Android Studio con Empty Activity (Kotlin).

🟩 4. Agregar Plugin y Dependencias

📄 build.gradle (Project)

plugins {
    id 'com.android.application' version '8.2.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.20' apply false
}

📄 build.gradle (Module: app)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'org.jetbrains.kotlin.plugin.serialization'
}

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
    implementation 'androidx.cardview:cardview:1.0.0'
    
    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    
    // Kotlinx Serialization
    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
    implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0'
    
    // OkHttp Logging
    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
    
    // Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
}

🟩 5. Agregar Permisos

📄 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:usesCleartextTraffic="true"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat.Light.DarkActionBar">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

🟩 6. Modelos con @Serializable

📄 Post.kt

package com.example.retrofitserializationapp

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Post(
    @SerialName("userId")
    val userId: Int,
    
    @SerialName("id")
    val id: Int = 0,
    
    @SerialName("title")
    val title: String,
    
    @SerialName("body")
    val body: String
)

📄 User.kt

package com.example.retrofitserializationapp

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class User(
    @SerialName("id")
    val id: Int,
    
    @SerialName("name")
    val name: String,
    
    @SerialName("username")
    val username: String,
    
    @SerialName("email")
    val email: String,
    
    @SerialName("phone")
    val phone: String? = null,
    
    @SerialName("website")
    val website: String? = null
)

📄 Comment.kt

package com.example.retrofitserializationapp

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Comment(
    @SerialName("postId")
    val postId: Int,
    
    @SerialName("id")
    val id: Int,
    
    @SerialName("name")
    val name: String,
    
    @SerialName("email")
    val email: String,
    
    @SerialName("body")
    val body: String
)

🟩 7. Interface API

📄 JsonPlaceholderApi.kt

package com.example.retrofitserializationapp

import retrofit2.Response
import retrofit2.http.*

interface JsonPlaceholderApi {

    // GET - Lista de posts
    @GET("posts")
    suspend fun getPosts(): Response<List<Post>>

    // GET - Post específico
    @GET("posts/{id}")
    suspend fun getPost(@Path("id") id: Int): Response<Post>

    // GET - Posts por usuario
    @GET("posts")
    suspend fun getPostsByUser(@Query("userId") userId: Int): Response<List<Post>>

    // GET - Lista de usuarios
    @GET("users")
    suspend fun getUsers(): Response<List<User>>

    // GET - Comentarios de un post
    @GET("posts/{id}/comments")
    suspend fun getComments(@Path("id") postId: Int): Response<List<Comment>>

    // POST - Crear post
    @POST("posts")
    suspend fun createPost(@Body post: Post): Response<Post>

    // PUT - Actualizar completo
    @PUT("posts/{id}")
    suspend fun updatePost(@Path("id") id: Int, @Body post: Post): Response<Post>

    // PATCH - Actualización parcial
    @PATCH("posts/{id}")
    suspend fun patchPost(@Path("id") id: Int, @Body post: Post): Response<Post>

    // DELETE - Eliminar post
    @DELETE("posts/{id}")
    suspend fun deletePost(@Path("id") id: Int): Response<Unit>
}

🟩 8. Cliente Retrofit

📄 RetrofitClient.kt

package com.example.retrofitserializationapp

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import java.util.concurrent.TimeUnit

object RetrofitClient {

    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    private val json = Json {
        ignoreUnknownKeys = true
        coerceInputValues = true
        isLenient = true
    }

    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
        .build()

    val api: JsonPlaceholderApi = retrofit.create(JsonPlaceholderApi::class.java)
}

🟩 9. Diseño XML

📄 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Retrofit + Kotlinx Serialization"
        android:textSize="20sp"
        android:textStyle="bold"
        android:textColor="#2196F3"
        android:padding="16dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="8dp">

        <Button
            android:id="@+id/btnGetPosts"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Posts"
            android:layout_margin="4dp" />

        <Button
            android:id="@+id/btnGetUsers"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Users"
            android:layout_margin="4dp" />

        <Button
            android:id="@+id/btnCreate"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Create"
            android:layout_margin="4dp" />

    </LinearLayout>

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone" />

    <TextView
        android:id="@+id/txtInfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textSize="14sp"
        android:textColor="#666666" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="8dp" />

</LinearLayout>

🟩 10. Layout del Item

📄 res/layout/item_post.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/txtId"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="ID: 1"
                android:textSize="12sp"
                android:textColor="#999999" />

            <TextView
                android:id="@+id/txtUserId"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="User: 1"
                android:textSize="12sp"
                android:textColor="#999999"
                android:layout_marginStart="16dp" />

        </LinearLayout>

        <TextView
            android:id="@+id/txtTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Título"
            android:textSize="18sp"
            android:textStyle="bold"
            android:textColor="#000000"
            android:layout_marginTop="8dp" />

        <TextView
            android:id="@+id/txtBody"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Cuerpo"
            android:textSize="14sp"
            android:textColor="#666666"
            android:layout_marginTop="8dp"
            android:maxLines="3"
            android:ellipsize="end" />

    </LinearLayout>

</androidx.cardview.widget.CardView>

🟩 11. Adaptador

📄 PostAdapter.kt

package com.example.retrofitserializationapp

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class PostAdapter(
    private var posts: List<Post> = emptyList(),
    private val onItemClick: (Post) -> Unit
) : RecyclerView.Adapter<PostAdapter.ViewHolder>() {

    fun updatePosts(newPosts: List<Post>) {
        posts = newPosts
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_post, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(posts[position])
    }

    override fun getItemCount(): Int = posts.size

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val txtId: TextView = itemView.findViewById(R.id.txtId)
        private val txtUserId: TextView = itemView.findViewById(R.id.txtUserId)
        private val txtTitle: TextView = itemView.findViewById(R.id.txtTitle)
        private val txtBody: TextView = itemView.findViewById(R.id.txtBody)

        fun bind(post: Post) {
            txtId.text = "ID: ${post.id}"
            txtUserId.text = "User: ${post.userId}"
            txtTitle.text = post.title
            txtBody.text = post.body

            itemView.setOnClickListener { onItemClick(post) }
        }
    }
}

🟩 12. MainActivity

📄 MainActivity.kt

package com.example.retrofitserializationapp

import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: PostAdapter
    private lateinit var progressBar: ProgressBar
    private lateinit var txtInfo: TextView
    private lateinit var btnGetPosts: Button
    private lateinit var btnGetUsers: Button
    private lateinit var btnCreate: Button

    private val api = RetrofitClient.api

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initializeViews()
        setupRecyclerView()
        setupListeners()
    }

    private fun initializeViews() {
        recyclerView = findViewById(R.id.recyclerView)
        progressBar = findViewById(R.id.progressBar)
        txtInfo = findViewById(R.id.txtInfo)
        btnGetPosts = findViewById(R.id.btnGetPosts)
        btnGetUsers = findViewById(R.id.btnGetUsers)
        btnCreate = findViewById(R.id.btnCreate)
    }

    private fun setupRecyclerView() {
        adapter = PostAdapter { post ->
            Toast.makeText(this, "Post: ${post.title}", Toast.LENGTH_SHORT).show()
        }
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter
    }

    private fun setupListeners() {
        btnGetPosts.setOnClickListener { getPosts() }
        btnGetUsers.setOnClickListener { getUsers() }
        btnCreate.setOnClickListener { createPost() }
    }

    private fun getPosts() {
        lifecycleScope.launch {
            try {
                showProgress(true)
                txtInfo.text = "Obteniendo posts..."

                val response = api.getPosts()

                showProgress(false)

                if (response.isSuccessful) {
                    val posts = response.body() ?: emptyList()
                    adapter.updatePosts(posts)
                    txtInfo.text = "✓ ${posts.size} posts cargados"
                    Toast.makeText(this@MainActivity, 
                        "Posts obtenidos: ${posts.size}", 
                        Toast.LENGTH_SHORT).show()
                } else {
                    txtInfo.text = "Error: ${response.code()}"
                    Toast.makeText(this@MainActivity, 
                        "Error: ${response.code()}", 
                        Toast.LENGTH_SHORT).show()
                }
            } catch (e: Exception) {
                showProgress(false)
                txtInfo.text = "Error: ${e.message}"
                Toast.makeText(this@MainActivity, 
                    "Error: ${e.message}", 
                    Toast.LENGTH_LONG).show()
            }
        }
    }

    private fun getUsers() {
        lifecycleScope.launch {
            try {
                showProgress(true)
                txtInfo.text = "Obteniendo usuarios..."

                val response = api.getUsers()

                showProgress(false)

                if (response.isSuccessful) {
                    val users = response.body() ?: emptyList()
                    
                    val userInfo = buildString {
                        append("✓ Usuarios obtenidos: ${users.size}\n\n")
                        users.take(5).forEach { user ->
                            append("• ${user.name} (@${user.username})\n")
                            append("  ${user.email}\n\n")
                        }
                    }
                    
                    txtInfo.text = userInfo
                    Toast.makeText(this@MainActivity, 
                        "Usuarios: ${users.size}", 
                        Toast.LENGTH_SHORT).show()
                } else {
                    txtInfo.text = "Error: ${response.code()}"
                }
            } catch (e: Exception) {
                showProgress(false)
                txtInfo.text = "Error: ${e.message}"
                Toast.makeText(this@MainActivity, 
                    "Error: ${e.message}", 
                    Toast.LENGTH_LONG).show()
            }
        }
    }

    private fun createPost() {
        lifecycleScope.launch {
            try {
                showProgress(true)
                txtInfo.text = "Creando post..."

                val newPost = Post(
                    userId = 1,
                    title = "Post con Kotlinx Serialization",
                    body = "Creado usando Retrofit y Kotlinx Serialization"
                )

                val response = api.createPost(newPost)

                showProgress(false)

                if (response.isSuccessful) {
                    val createdPost = response.body()
                    txtInfo.text = "✓ Post creado con ID: ${createdPost?.id}"
                    Toast.makeText(this@MainActivity, 
                        "Post creado: ${createdPost?.id}", 
                        Toast.LENGTH_SHORT).show()
                } else {
                    txtInfo.text = "Error: ${response.code()}"
                }
            } catch (e: Exception) {
                showProgress(false)
                txtInfo.text = "Error: ${e.message}"
                Toast.makeText(this@MainActivity, 
                    "Error: ${e.message}", 
                    Toast.LENGTH_LONG).show()
            }
        }
    }

    private fun showProgress(show: Boolean) {
        progressBar.visibility = if (show) View.VISIBLE else View.GONE
    }
}

🟩 13. Configuración Json

val json = Json {
    ignoreUnknownKeys = true    // Ignora campos JSON no mapeados
    coerceInputValues = true    // Convierte null en valores default
    isLenient = true           // Permite JSON no estricto
    prettyPrint = true         // JSON formateado (debug)
    encodeDefaults = true      // Incluye valores default
}

🟩 14. Anotaciones Útiles

@Serializable
data class Example(
    // Nombre personalizado
    @SerialName("user_id")
    val userId: Int,
    
    // Campo opcional
    val phone: String? = null,
    
    // Valor por defecto
    val active: Boolean = true,
    
    // Transient (no serializar)
    @Transient
    val tempData: String = ""
)

🟩 15. Comparación Final

AspectoKotlinx SerializationGson
Rendimiento⚡⚡⚡⚡⚡
Type-safety
Multiplatform
Tamaño APKMenorMayor
Curva aprendizajeMediaBaja

▶️ Cómo Ejecutar

  1. Abre Android Studio
  2. Agrega el plugin de serialization
  3. Copia el código
  4. Sync Gradle
  5. Ejecuta y prueba

🧪 Resultado Final

App que consume API REST con Kotlinx Serialization, más rápida y type-safe que Gson.

📥 Descargar Proyecto

👉 

🙌 Gracias por Visitar mi Blog

✔️ Compártelo
✔️ Déjame un comentario
✔️ Sígueme para más contenido

❓ Preguntas Frecuentes

1. ¿Es mejor que Gson?
Sí, Kotlinx Serialization es más rápida, type-safe y optimizada para Kotlin.

2. ¿Funciona con Java?
No, está diseñada específicamente para Kotlin.

3. ¿Necesito plugin especial?
Sí, el plugin kotlin-serialization es obligatorio.

4. ¿Puedo migrar desde Gson?
Sí, cambia @SerializedName por @SerialName y agrega @Serializable.

No hay comentarios:

Publicar un comentario