Salta el contingut

Guia Simplificada: DialogFragment amb ViewModel

Estructura del Projecte

app/
├── data/
│   └── TasksList.kt
├── model/
│   └── Task.kt
├── viewmodel/
│   └── TasksViewModel.kt
├── ui/
│   ├── fragments/
│   │   └── HomeFragment.kt
│   └── dialogs/
│       ├── AfegirTascaDialog.kt
└── adapter/
    └── TasksAdapter.kt

Flux de Treball

                ┌─────────────────────────────────────────┐
                │           TasksList (Object)            │
                │      MutableList<Task> (Singleton)      │
                └──────────────────┬──────────────────────┘
                                   ▲
                                   │ modifica
                                   │
┌──────────────────────────────────┴─────────────────────────────────────────────────┐
│                          TasksViewModel                                            │
│  • Valida les dades                                                                │
│  • Modifica TasksList                                                              │
│  • Emet LiveData<TascaAction> (AFEGIDA/MODIFICADA/ELIMINADA)                       │
│  • Emet LiveData<String?> (errorMessage)                                           │
└───┬─────────────────────────────────────────┬─────────────────┬────────────────────┘
    │                                         ▲                 │
    │  observa tascaActionEvent               │ AfegirTasca VM  │ observa errorMessage
    │                                         │                 │
    ▼                                         │                 ▼
┌──────────────────────────────────┐         ┌──────────────────────────────────────┐
│        HomeFragment              │         │   AfegirTascaDialog                  │
│                                  │         │                                      │
│  • Observa tascaActionEvent      │         │  • Observa errorMessage              │
│  • Actualitza RecyclerView       │────────▶│  • Mostra Toast d'error              │
│  • Mostra Toast de confirmació   │         │  • Crida viewModel                   │
│  • Crida dialogs                 │         │    amb dades del formulari           │
│                                  │         │                                      │
└──────────────────────────────────┘         └──────────────────────────────────────┘

1. Model de Dades

// Task.kt
package cat.ivha.sparklestask.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class Task(
    val id: Int,
    val title: String,
    val points: Int,
    val data: Long
) : Parcelable

2. Llista de Tasques (Object)

// TasksList.kt
package cat.ivha.sparklestask.data

import cat.ivha.sparklestask.model.Task

object TasksList {
    val items = mutableListOf<Task>(
        Task(1, "Tasca 1", 10, System.currentTimeMillis()),
        Task(2, "Tasca 2", 20, System.currentTimeMillis()),
        Task(3, "Tasca 3", 30, System.currentTimeMillis())
    )

    private var nextId = 4

    fun getNextId(): Int = nextId++

    fun addTask(task: Task) {
        items.add(task)
    }


    fun getTaskById(id: Int): Task? {
        return items.find { it.id == id }
    }
}

3. ViewModel

// TasksViewModel.kt
package cat.ivha.sparklestask.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import cat.ivha.sparklestask.data.TasksList
import cat.ivha.sparklestask.model.Task

class TasksViewModel : ViewModel() {

    // Tipus d'acció realitzada
    enum class TascaAction {
        AFEGIDA, MODIFICADA, ELIMINADA, NONE
    }

    // Event per notificar canvis
    private val _tascaActionEvent = MutableLiveData<TascaAction>()
    val tascaActionEvent: LiveData<TascaAction> = _tascaActionEvent

    // Missatge d'error si la validació falla
    private val _errorMessage = MutableLiveData<String?>()
    val errorMessage: LiveData<String?> = _errorMessage

    /**
     * Afegeix una nova tasca amb validació
     */
    fun afegirTasca(title: String, points: String, data: String): Boolean {
        // Validació
        if (title.isBlank()) {
            _errorMessage.value = "El nom és obligatori"
            return false
        }

        val pointsInt = points.toIntOrNull()
        if (pointsInt == null) {
            _errorMessage.value = "Els punts han de ser un número"
            return false
        }

        val dataLong = data.toLongOrNull()
        if (dataLong == null) {
            _errorMessage.value = "La data no és vàlida"
            return false
        }

        // Crear i afegir la tasca
        val novaTasca = Task(
            id = TasksList.getNextId(),
            title = title,
            points = pointsInt,
            data = dataLong
        )

        TasksList.addTask(novaTasca)
        _tascaActionEvent.value = TascaAction.AFEGIDA
        _errorMessage.value = null
        return true
    }


    /**
     * Reseteja l'event
     */
    fun resetEvent() {
        _tascaActionEvent.value = TascaAction.NONE
    }

    /**
     * Reseteja el missatge d'error
     */
    fun resetError() {
        _errorMessage.value = null
    }
}

4. DialogFragment per AFEGIR

// AfegirTascaDialog.kt
package cat.ivha.sparklestask.ui.dialogs

import android.app.Dialog
import android.os.Bundle
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import cat.ivha.sparklestask.R
import cat.ivha.sparklestask.viewmodel.TasksViewModel

class AfegirTascaDialog : DialogFragment() {

    private val viewModel: TasksViewModel by activityViewModels()

    private lateinit var etNom: EditText
    private lateinit var etPoints: EditText
    private lateinit var etData: EditText
    private lateinit var btnGuardar: Button
    private lateinit var btnCancelar: Button

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(requireContext())
        val inflater = requireActivity().layoutInflater
        val view = inflater.inflate(R.layout.dialog_afegir_tasca, null)

        initViews(view)
        setupListeners()
        observeViewModel()

        builder.setView(view)
        return builder.create()
    }

    private fun initViews(view: android.view.View) {
        etNom = view.findViewById(R.id.etNom)
        etPoints = view.findViewById(R.id.etPoints)
        etData = view.findViewById(R.id.etData)
        btnGuardar = view.findViewById(R.id.btnGuardar)
        btnCancelar = view.findViewById(R.id.btnCancelar)

        // Valor per defecte
        etData.setText(System.currentTimeMillis().toString())
    }

    private fun setupListeners() {
        btnGuardar.setOnClickListener {
            val title = etNom.text.toString()
            val points = etPoints.text.toString()
            val data = etData.text.toString()

            // Crida al ViewModel (ell valida)
            val success = viewModel.afegirTasca(title, points, data)

            if (success) {
                dismiss()
            }
        }

        btnCancelar.setOnClickListener {
            dismiss()
        }
    }

    private fun observeViewModel() {
        viewModel.errorMessage.observe(this) { error ->
            error?.let {
                Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show()
                viewModel.resetError()
            }
        }
    }

    override fun onStart() {
        super.onStart()
        dialog?.window?.setLayout(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
        dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent)
    }
}

5. HomeFragment

// HomeFragment.kt
package cat.ivha.sparklestask.ui.fragments

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import cat.ivha.sparklestask.R
import cat.ivha.sparklestask.adapter.TasksAdapter
import cat.ivha.sparklestask.data.TasksList
import cat.ivha.sparklestask.ui.dialogs.AfegirTascaDialog
import cat.ivha.sparklestask.ui.dialogs.ModificarTascaDialog
import cat.ivha.sparklestask.viewmodel.TasksViewModel

class HomeFragment : Fragment(R.layout.home_rv) {

    private val viewModel: TasksViewModel by activityViewModels()

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: TasksAdapter
    private lateinit var btnAfegir: Button

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.home_rv, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        initComponents(view)
        setupRecyclerView()
        setupListeners()
        observeViewModel()
    }

    private fun initComponents(view: View) {
        recyclerView = view.findViewById(R.id.rvTasques)
        btnAfegir = view.findViewById(R.id.btnAfegir)
    }

    private fun setupRecyclerView() {
        recyclerView.layoutManager = LinearLayoutManager(requireContext())

        adapter = TasksAdapter(
            itemsComplets = TasksList.items
        )

        recyclerView.adapter = adapter
    }

    private fun setupListeners() {
        btnAfegir.setOnClickListener {
            AfegirTascaDialog().show(parentFragmentManager, "AfegirTascaDialog")
        }
    }

    private fun observeViewModel() {
        viewModel.tascaActionEvent.observe(viewLifecycleOwner) { action ->
            when (action) {
                TasksViewModel.TascaAction.AFEGIDA -> {
                    adapter.updateTasks(TasksList.items)                    
                    Toast.makeText(requireContext(), "Tasca afegida!", Toast.LENGTH_SHORT).show()
                    viewModel.resetEvent()
                }
                TasksViewModel.TascaAction.MODIFICADA -> {
                }
                TasksViewModel.TascaAction.ELIMINADA -> {
                }
                TasksViewModel.TascaAction.NONE -> {
                    // No fer res
                }
            }
        }
    }
}

6. Adapter

// TasksAdapter.kt
package cat.ivha.sparklestask.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import cat.ivha.sparklestask.R
import cat.ivha.sparklestask.model.Task

class TasksAdapter(
    private var itemsComplets: List<Task>
) : RecyclerView.Adapter<TasksAdapter.TaskViewHolder>() {

    class TaskViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val tvTitle: TextView = view.findViewById(R.id.tvTitle)
        val tvPoints: TextView = view.findViewById(R.id.tvPoints)
    }

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

    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        val task = itemsComplets[position]

        holder.tvTitle.text = task.title
        holder.tvPoints.text = "Points: ${task.points}"
    }

    override fun getItemCount(): Int = itemsComplets.size

    fun updateTasks(newTasks: List<Task>) {
        itemsComplets = newTasks
        notifyDataSetChanged()
    }
}

7. Layouts

dialog_afegir_tasca.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="wrap_content"
    android:orientation="vertical"
    android:padding="24dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Afegir Nova Tasca"
        android:textSize="20sp"
        android:textStyle="bold"
        android:layout_marginBottom="16dp"/>

    <EditText
        android:id="@+id/etNom"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Nom de la tasca"
        android:layout_marginBottom="12dp"/>

    <EditText
        android:id="@+id/etPoints"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Points"
        android:inputType="number"
        android:layout_marginBottom="12dp"/>

    <EditText
        android:id="@+id/etData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Data (timestamp)"
        android:inputType="number"
        android:layout_marginBottom="16dp"/>

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

        <Button
            android:id="@+id/btnCancelar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Cancel·lar"
            style="@style/Widget.Material3.Button.TextButton"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/btnGuardar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Guardar"/>
    </LinearLayout>

</LinearLayout>

8. Dependències (build.gradle)

dependencies {
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
    implementation "androidx.fragment:fragment-ktx:1.6.2"

    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"

    // Material Design
    implementation "com.google.android.material:material:1.10.0"
}

// Al principi del fitxer
plugins {
    id 'kotlin-parcelize'
}