Bienvenido a Universo Android

Estamos actualizando el sitio web! Pronto subiremos el nuevo diseño de la web! Gracias por su paciencia!

Mostrando entradas con la etiqueta alternativa Gson. Mostrar todas las entradas
Mostrando entradas con la etiqueta alternativa Gson. Mostrar todas las entradas

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-safetyNo
MultiplatformNo
Compile-timeReflexió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.