JSON en Kotlin con ViewModel

Uso de JSON en Kotlin con ViewModel, corutinas y OkHttp

Partiendo del ejercicio JSON en Kotlin, vamos a modificarlo para utilizar ViewModel y dentro de él descargar el fichero JSON desde una web usando OkHttp dentro de una corutina

Gemini:

modify the code of the app for using viewmodel and in the viewmodel download the file dam.org.es/ficheros/contactos.json  using okhttp inside a coroutine

Resultado

Read build.gradle.kts
Read libs.versions.toml
Looked up the latest versions of okhttp, lifecycle-viewmodel-ktx, lifecycle-livedata-ktx, activity-ktx
Edited libs.versions.toml
Edited build.gradle.kts
Read AndroidManifest.xml
Edited AndroidManifest.xml
Read Persona.kt
Read Analisis.kt
Edited MainViewModel.kt
Edited MainViewModel.kt
Read ContactosAdapter.kt
Edited MainActivity.kt
Ran Gradle sync
Analyzed current file
Read MainActivity.kt

I have updated the application to use a ViewModel for managing data and performing network operations. Here's a summary of the changes:
1. Dependencies: Added OkHttp, ViewModel, LiveData, and Activity-KTX to libs.versions.toml and build.gradle.kts.
2. Permissions: Added INTERNET permission and enabled usesCleartextTraffic in AndroidManifest.xml (required for the http URL).
3. ViewModel: Created MainViewModel.kt which uses viewModelScope and OkHttp to download the JSON file in a background thread (via Dispatchers.IO) and updates LiveData with the results.
4. Activity: Refactored MainActivity.kt to observe the ViewModel's LiveData. It now triggers the download when the button is clicked and updates the RecyclerView automatically.
The app now follows the MVVM pattern and correctly handles network operations using coroutines and OkHttp.

 

Ficheros para colocar en el servidor web en Internet

Contactos.json

{
"contactos":[
  {
  "nombre": "Penélope Cruz",
  "direccion":"calle los remedios, 29007, malaga",
  "email":"angie@gmail.com",
  "telefono":{
    "casa": "952245407",
    "movil":"603459530",
    "trabajo":"951230246"
    }
  }
  ,
  {
  "nombre": "Blanca Suárez",
  "direccion":"calle hilera, 29010, malaga",
  "email":"nikole@gmail.com",
  "telefono":{
    "casa": "952233407",
    "movil":"625459540",
    "trabajo":"951230246"
    }
  }
,
  {
  "nombre": "Adriana Ugarte",
  "direccion":"calle larios, 29002, malaga",
  "email":"jenny@gmail.com",
  "telefono":{
    "casa": "951065407",
    "movil":"628659550",
    "trabajo":"952002146"
    }
  }
  ]
}

Contacts.json

{
  "contactos": [
    {
      "nombre": "Angelina Jolie",
      "direccion": "calle los remedios, 29007, malaga",
      "email": "angie@gmail.com",
      "telefono": {
        "casa": "952245407",
        "movil": "603459530",
        "trabajo": "951230246"
      }
    },
    {
      "nombre": "Nikole Kidman",
      "direccion": "calle hilera, 29010, malaga",
      "email": "nikole@gmail.com",
      "telefono": {
        "casa": "952233407",
        "movil": "625459540",
        "trabajo": "951230246"
      }
    },
    {
      "nombre": "Jennifer Lopez",
      "direccion": "calle larios, 29002, malaga",
      "email": "jenny@gmail.com",
      "telefono": {
        "casa": "951065407",
        "movil": "628659550",
        "trabajo": "952002146"
      }
    }
  ]
}

Crear con Android Studio el proyecto ListaContactosDescarga

Dar el permiso de acceso a Internet en el manifiesto

AndroidManifest.xml

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

Modificar libs.versions.toml para añadir las bibliotecas a utilizar: GSON, viewmodel, livedata corutinas y okhttp

[versions]
agp = "8.13.2"
kotlin = "2.1.0"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.13.0"
activity = "1.12.4"
constraintlayout = "2.2.1"
gson = "2.13.2"
okhttp = "4.12.0"
lifecycle = "2.10.0"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

Modificar build.gradle (del módulo) para añadir view binding y las bibliotecas a utilizar: GSON, viewmodel, livedata corutinas y okhttp

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
}

android {
    namespace = "com.example.listacontactosdescarga"
    compileSdk {
        version = release(36)
    }

    buildFeatures {
        viewBinding = true
    }

    defaultConfig {
        applicationId = "com.example.listacontactosdescarga"
        minSdk = 28
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }

}

dependencies {
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    implementation(libs.androidx.activity)
    implementation(libs.androidx.constraintlayout)

    implementation (libs.gson)
    implementation (libs.okhttp)
    implementation (libs.androidx.lifecycle.viewmodel.ktx)
    implementation (libs.androidx.lifecycle.livedata.ktx)
    implementation (libs.androidx.activity.ktx)

    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

modelo/Telefono

data class Telefono(
    @SerializedName("casa")
    val casa: String,
    @SerializedName("movil")
    val movil: String,
    @SerializedName("trabajo")
    val trabajo: String
)

modelo/Contacto

data class Contacto(
    @SerializedName("nombre")
    val nombre: String,
    @SerializedName("direccion")
    val direccion: String,
    @SerializedName("email")
    val email: String,
    @SerializedName("telefono")
    val telefono: Telefono
)

modelo/Persona

data class Persona(
    @SerializedName("contactos")
    val contactos: ArrayList<Contacto>
)

item_contact.xml

data class Persona(
    @SerializedName("contactos")
    val contactos: ArrayList<Contacto>
)

adapter/ContactosAdapter

class ContactosAdapter(private var mContactos: List<Contacto>) : RecyclerView.Adapter<ContactosAdapter.ViewHolder>() {

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val binding = ItemContactBinding.bind(itemView)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ViewHolder {
        val context = parent.context
        val inflater = LayoutInflater.from(context)
        val contactView = inflater.inflate(R.layout.item_contact, parent, false)

        return ViewHolder(contactView)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // Get the data model based on position
        val contact = mContactos[position]

        // Set item views based on your views and data model
        holder.binding.contactName.text = contact.nombre
        holder.binding.mobilePhone.text = contact.telefono.movil
    }

    // Returns the total count of items in the list
    override fun getItemCount(): Int {
        return mContactos.size
    }

    fun actualizar(data: List<Contacto>) {
        mContactos = data
        notifyDataSetChanged()
    }
}

utils/Analisis

object Analisis {

    @Throws(JSONException::class)
    fun analizarContactos(cadena: String?): ArrayList<Contacto> {
        val jAcontactos: JSONArray
        val objeto: JSONObject
        var jOcontacto: JSONObject
        var jOtelefono: JSONObject
        var contacto: Contacto
        var telefono: Telefono
        val personas = ArrayList<Contacto>()
        // añadir contactos (en formato JSON) a personas
        objeto = JSONObject(cadena)
        jAcontactos = objeto.getJSONArray("contactos")
        for (i in 0 until jAcontactos.length()) {
            jOcontacto = jAcontactos.getJSONObject(i)
            jOtelefono = jOcontacto.getJSONObject("telefono")
            telefono = Telefono(jOtelefono.getString("casa"), jOtelefono.getString("movil"), jOtelefono.getString("trabajo"))
            contacto = Contacto(jOcontacto.getString("nombre"), jOcontacto.getString("direccion"), jOcontacto.getString("email"), telefono)
            personas.add(contacto)
        }
        return personas
    }
}

MainViewModel

class MainViewModel : ViewModel() {

    private val _contactos = MutableLiveData<List<Contacto>>()
    val contactos: LiveData<List<Contacto>> get() = _contactos

    private val _error = MutableLiveData<String>()
    val error: LiveData<String> get() = _error

    private val _mensaje = MutableLiveData<String>()
    val mensaje: LiveData<String> get() = _mensaje

    private val client = OkHttpClient()

    fun descargarContactos(url:String, usarGson: Boolean) {
        viewModelScope.launch {
            try {
                val result = withContext(Dispatchers.IO) {
                    val request = Request.Builder().url(url).build()

                    client.newCall(request).execute().use { response ->
                        if (!response.isSuccessful)
                            throw IOException("Unexpected code $response")
                        response.body?.string() ?: ""

                    }
                }

                _mensaje.value = "Contactos descargados OK"

                val lista = if (usarGson) {
                    val persona = Gson().fromJson(result, Persona::class.java)
                    persona.contactos
                } else {
                    Analisis.analizarContactos(result)
                }
                _contactos.value = lista
            } catch (e: Exception) {
                _error.value = e.message ?: "Error desconocido"
            }
        }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Lista de contactos"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.022" />

    <Switch
        android:id="@+id/switch1"
        android:layout_width="94dp"
        android:layout_height="48dp"
        android:checked="false"
        android:text="Gson"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.95"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:layout_marginBottom="16dp"
        android:text="Mostrar"
        app:layout_constraintBottom_toTopOf="@+id/recyclerView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.462"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="362dp"
        android:layout_height="561dp"
        android:layout_marginBottom="28dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.326"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity

class MainActivity : AppCompatActivity(), View.OnClickListener {
    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: ContactosAdapter
    private val viewModel: MainViewModel by viewModels()

    companion object {
        const val CONTACTOS = "https://dam.org.es/ficheros/contactos.json"
        const val CONTACTS = "https://dam.org.es/ficheros/contacts.json"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.button.setOnClickListener(this)

        setupRecyclerView()
        observeViewModel()
    }

    private fun setupRecyclerView() {
        adapter = ContactosAdapter(emptyList())
        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
    }

    private fun observeViewModel() {
        viewModel.contactos.observe(this) { contactos ->
            if (contactos.isNotEmpty()) {
                adapter.actualizar(contactos)
            } else {
                mostrarMensaje("No se encontraron contactos")
            }
        }

        viewModel.error.observe(this) { errorMessage ->
            mostrarMensaje("Error: $errorMessage")
        }

        viewModel.mensaje.observe(this) { texto ->
            // Call your local function here
            mostrarMensaje(texto)
        }
    }

    override fun onClick(v: View) {
        if (v === binding.button) {
            if (!binding.switch1.isChecked)
                viewModel.descargarContactos(CONTACTOS, false)
            else
                viewModel.descargarContactos(CONTACTS, true)
        }
    }

    private fun mostrarMensaje(texto: String) {
        Toast.makeText(this, texto, Toast.LENGTH_SHORT).show()
    }
}

Código en GitHub

Comprobar posibles errores al descargar el archivo json:

const val CONTACTOS = "https://dam.org.es/ficheros/contactosmal.json"

Deja una respuesta