Ejercicio con Fragments: NotesFragment

Realización de un ejercicio con Fragments

Crear una aplicación que use 2 fragments: uno para mostrar una lista de notas (recordatorios, con título y descripción) y otro para ver los detalles del elemento pulsado

Diagrama y Explicación de Comunicación entre Componentes Android

(creado con Gemini)

Explicación del patrón de diseño recomendado para la comunicación entre los fragments ListNoteFragment, ViewNoteFragment y su actividad contenedora, MainActivity, garantizando un código desacoplado y mantenible.

Explicación del Flujo

Este patrón de diseño es robusto y sigue las mejores prácticas de desarrollo en Android para la comunicación entre fragments y actividades.

1. Comunicación de ListNoteFragment a MainActivity (Fragment a Actividad)

  • El Problema: Un Fragment no debe tener conocimiento directo de la Activity que lo contiene para ser reutilizable. Acoplarlo a una actividad específica limita su uso en otros contextos.
  • La Solución (Patrón de Callback):
    1. Se define una interfaz en ListNoteFragment (por ejemplo, ListNoteFragmentCallback). Esta interfaz actúa como un «contrato» que define las acciones que el fragment necesita comunicar hacia el exterior (como onNoteView(note)).
    2. MainActivity implementa esta interfaz. Al hacerlo, se compromete a proporcionar una lógica concreta para los métodos de esa interfaz.
    3. En el método onAttach() del fragment, se obtiene una referencia al contexto (que es la Activity) y se realiza un «casting» a la interfaz ListNoteFragmentCallback. Esto asegura en tiempo de ejecución que la actividad anfitriona cumpla con el contrato.
    4. Cuando un usuario interactúa con la lista (ej: clic en un item), el Fragment no intenta abrir ViewNoteFragment directamente. En su lugar, invoca el método de la interfaz (callback.onNoteView(note)).
    5. MainActivity, que ha implementado la interfaz, recibe esta llamada y es la responsable de orquestar la siguiente acción: crear y mostrar ViewNoteFragment.

2. Comunicación de MainActivity a ViewNoteFragment (Actividad a Fragment)

  • El Problema: ¿Cómo pasamos de forma segura la nota seleccionada desde la Activity al nuevo Fragment que se va a mostrar?
  • La Solución (Argumentos del Fragment):
    1. Cuando MainActivity recibe el evento de ListNoteFragment, inicia una transacción de fragments para mostrar ViewNoteFragment.
    2. Para pasar la nota seleccionada, la Activity no llama a un método público del fragment. En su lugar, empaqueta los datos necesarios (el objeto Note si es Parcelable o su ID) en un objeto Bundle.
    3. Este Bundle se adjunta al ViewNoteFragment usando el método fragment.setArguments(bundle).
    4. Dentro de ViewNoteFragment (típicamente en onCreate() o onViewCreated()), se recupera el Bundle de los argumentos para acceder a los datos de la nota y mostrarlos en la interfaz de usuario.

Resumen de Roles

MainActivity: Actúa como el coordinador principal o el «director de orquesta». Carga ListNoteFragment en el contenedor de fragments, escucha los eventos de ListNoteFragment y decide cómo responder, en este caso realiza una transición mostrando ViewNoteFragment y pasándole los datos correctos.

ListNoteFragment: Su única responsabilidad es mostrar una lista de notas y notificar (a través de la interfaz) cuando se selecciona un elemento. No sabe qué pasará después.

ViewNoteFragment: Su única responsabilidad es recibir datos a través de sus argumentos y mostrarlos. No sabe de dónde vienen los datos ni qué fragment los originó.

Este desacoplamiento hace que el código sea más modular, fácil de probar y mucho más sencillo de mantener y escalar a largo plazo.

 

Crear el proyecto NotesFragment

 

Añadir al build.gradle del módulo:

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

    id("kotlin-parcelize")
}

android {
    namespace = "com.example.notesfragment"
    compileSdk = 36

    buildFeatures {
        viewBinding = true
    }

build.gradle.kts (Module :app)

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

    id("kotlin-parcelize")
}

android {
    namespace = "com.example.notesfragment"
    compileSdk = 36

    buildFeatures {
        viewBinding = true
    }

    defaultConfig {
        applicationId = "com.example.notesfragment"
        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)
    
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

Modelo Note

@Parcelize
data class Note(val title: String, val content: String) : Parcelable

NotesProvider

class NotesProvider {
    companion object {
        //Se construye la lista de notas
        var list = mutableListOf<Note>(
            Note("Compras", "Comprar una cubierta para la bici"),
            Note("Supermercado", "Comprar frutas y verduras"),
            Note("Médico", "Pedir cita al médico"),
            Note("Trabajo", "Preparar ejercicios"),
            Note("Excursión", "Planificar la excursión del fin de semana")
        )
    }
}

interface OnItemClickListener

fun interface OnItemClickListener {
    fun onItemClick(note: Note?)
}

NoteViewHolder

class NoteViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    var binding: ItemNoteBinding = ItemNoteBinding.bind(view)

    fun render(note: Note, listener: OnItemClickListener) {
        binding.title.text = note.title
        binding.content.text = note.content
        itemView.setOnClickListener { listener.onItemClick(note) }
    }

}

res/layout/item_note.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="wrap_content" android:layout_marginStart="16dp">

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_marginTop="16dp"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/title"
        app:layout_constraintTop_toBottomOf="@+id/title" />

</androidx.constraintlayout.widget.ConstraintLayout>

NoteAdapter

class NoteAdapter(
    private val list: MutableList<Note>,
    private val listener: OnItemClickListener) :
    RecyclerView.Adapter<NoteViewHolder?>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
        val itemView: View = LayoutInflater.from(parent.context)
                            .inflate(R.layout.item_note, parent, false)

        return NoteViewHolder(itemView)
    }

    //Método que vincula cada dato con la vista
    public override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
        val note: Note = list[position]

        holder.render(note, listener)
    }

    override fun getItemCount(): Int {
        return list.size
    }
}

res/layout/fragment_view_note.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:id="@+id/lly_sector_edit_padding"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="56dp"
        android:textAppearance="@android:style/TextAppearance.Material.Medium"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvContent"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvTitle" />


</androidx.constraintlayout.widget.ConstraintLayout>

ViewNoteFragment

class ViewNoteFragment : Fragment() {
    private lateinit var binding: FragmentViewNoteBinding
    private lateinit var callback: ListNoteFragmentCallback

    fun newInstance(bundle: Bundle?): Fragment {
        val fragment = ViewNoteFragment()

        if (bundle != null) {
            fragment.setArguments(bundle)
        }
        return fragment
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentViewNoteBinding.inflate(inflater, container, false)
        return binding.root
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun onViewCreated(@NonNull view: View, @Nullable savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        if (getArguments() != null) {
            val note: Note? =
                requireArguments().getParcelable(MainActivity.TAG_NOTE, Note::class.java)
            binding.tvTitle.text = note?.title
            binding.tvContent.text = note?.content
        }

    }

}

ListNoteFragmentCallback

interface ListNoteFragmentCallback {
    fun onNoteView(note: Note?)
}

fragment_list_note.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvnote"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</androidx.constraintlayout.widget.ConstraintLayout>

ListNoteFragment

class ListNoteFragment : Fragment() {

    private lateinit var binding: FragmentListNoteBinding
    lateinit private var listener: OnItemClickListener
    lateinit private var callback: ListNoteFragmentCallback

    companion object {
        const val TAG: String = "ListNoteFragment"
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentListNoteBinding.inflate(inflater, container, false)
        return binding.root
    }


    public override fun onViewCreated(@NonNull view: View, @Nullable savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.rvnote.setLayoutManager(
            LinearLayoutManager(
                getActivity(),
                LinearLayoutManager.VERTICAL,
                false
            )
        )
        // This works because OnItemClickListener is a functional (or SAM - Single Abstract Method) interface.
        // Kotlin allows you to represent such interfaces with a lambda, making the code much cleaner and more readable
        listener = OnItemClickListener { note -> callback.onNoteView(note) }

        binding.rvnote.setAdapter(NoteAdapter(NotesProvider.list, listener))
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)

        try {
            callback = context as ListNoteFragmentCallback
        } catch (e: ClassCastException) {
            throw ClassCastException("$context must implement ListNoteFragmentCallback")
        }
    }

    override fun onDetach() {
        super.onDetach()
        // listener = null
    }

}

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>

    <color name="colorPrimary">#2196F3</color>
    <color name="colorPrimaryDark">#1976D2</color>
    <color name="colorPrimaryLight">#BBDEFB</color>
    <color name="colorAccent">#FF4081</color>
</resources>

dimens.xml

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <dimen name="margin_fab">16dp</dimen>
    <dimen name="elevation_fab">6dp</dimen>

</resources>

strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">NotesFragment</string>

    <string name="action_order">Ordernar por</string>
    <string name="menu_aboutus">Sobre nosotros</string>
    <string name="menu_help">Ayuda</string>
    <string name="menu_settings">Configuración</string>
    <string name="action_share">Compartir</string>
    <string name="add_a_new_note">Add a new note</string>
</resources>
res/drawable/ic_action_add.png  res/drawable/ic_action_order.png  res/drawable/ic_action_share.png
add
order
share

res/menu/menu_main.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_order"
        android:icon="@drawable/ic_action_order"
        android:title="@string/action_order"
        app:showAsAction="ifRoom">

    </item>
    <item
        android:id="@+id/action_share"
        android:icon="@drawable/ic_action_share"
        android:title="@string/action_share"
        app:showAsAction="always">

    </item>

    <item
        android:id="@+id/action_settings"
        android:title="@string/menu_settings" />
    <item
        android:id="@+id/action_help"
        android:title="@string/menu_help" />
    <item
        android:id="@+id/action_aboutus"
        android:title="@string/menu_aboutus" />

</menu>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    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="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            android:elevation="4dp"
            android:minHeight="?attr/actionBarSize"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    </com.google.android.material.appbar.AppBarLayout>

    <FrameLayout
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:id="@+id/fragmentcontainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/margin_fab"
        android:clickable="true"
        android:contentDescription="@string/add_a_new_note"
        android:elevation="@dimen/elevation_fab"
        app:srcCompat="@drawable/ic_action_add" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

MainActivity

class MainActivity : AppCompatActivity(), ListNoteFragmentCallback {
    private lateinit var binding: ActivityMainBinding

    lateinit private var viewNoteFragment: ViewNoteFragment

    companion object {
        const val TAG_NOTE: String = "Note"
        const val TAG_VIEWNOTEFRAGMENT: String = "ViewNoteFragment"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(R.layout.activity_main)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        // toolbar = findViewById(R.id.toolbar)
        setSupportActionBar(binding.toolbar)

        val fm: FragmentManager = getSupportFragmentManager()
        val ft: FragmentTransaction = fm.beginTransaction()
        ft.replace(R.id.fragmentcontainer, ListNoteFragment(), ListNoteFragment.TAG)
        ft.addToBackStack(null)
        ft.commit()
    }
    
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }
    

    override fun onNoteView(note: Note?) {
        val fragmentManager: FragmentManager = supportFragmentManager
        lateinit var fragmentTransaction: FragmentTransaction
        lateinit var bundle: Bundle

        if (fragmentManager.findFragmentByTag(TAG_VIEWNOTEFRAGMENT) == null) {
            viewNoteFragment = ViewNoteFragment()
        }

        if (note != null) {
            bundle = Bundle()
            bundle.putParcelable(TAG_NOTE, note)
            viewNoteFragment = (viewNoteFragment.newInstance(bundle) as ViewNoteFragment?)!!
        }

        fragmentTransaction = fragmentManager.beginTransaction()
        fragmentTransaction.add(R.id.fragmentcontainer, viewNoteFragment, TAG_VIEWNOTEFRAGMENT)
        fragmentTransaction.replace(R.id.fragmentcontainer, viewNoteFragment, TAG_VIEWNOTEFRAGMENT)
        fragmentTransaction.addToBackStack(null)
        fragmentTransaction.commit()
    }

}

 

 

Deja una respuesta