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
Fragmentno debe tener conocimiento directo de laActivityque lo contiene para ser reutilizable. Acoplarlo a una actividad específica limita su uso en otros contextos. - La Solución (Patrón de Callback):
- 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 (comoonNoteView(note)). MainActivityimplementa esta interfaz. Al hacerlo, se compromete a proporcionar una lógica concreta para los métodos de esa interfaz.- En el método
onAttach()del fragment, se obtiene una referencia al contexto (que es la Activity) y se realiza un «casting» a la interfazListNoteFragmentCallback. Esto asegura en tiempo de ejecución que la actividad anfitriona cumpla con el contrato. - Cuando un usuario interactúa con la lista (ej: clic en un item), el
Fragmentno intenta abrirViewNoteFragmentdirectamente. En su lugar, invoca el método de la interfaz (callback.onNoteView(note)). MainActivity, que ha implementado la interfaz, recibe esta llamada y es la responsable de orquestar la siguiente acción: crear y mostrarViewNoteFragment.
- Se define una interfaz en
2. Comunicación de MainActivity a ViewNoteFragment (Actividad a Fragment)
- El Problema: ¿Cómo pasamos de forma segura la nota seleccionada desde la
Activityal nuevoFragmentque se va a mostrar? - La Solución (Argumentos del Fragment):
- Cuando
MainActivityrecibe el evento deListNoteFragment, inicia una transacción de fragments para mostrarViewNoteFragment. - Para pasar la nota seleccionada, la
Activityno llama a un método público del fragment. En su lugar, empaqueta los datos necesarios (el objetoNotesi esParcelableo su ID) en un objetoBundle. - Este
Bundlese adjunta alViewNoteFragmentusando el métodofragment.setArguments(bundle). - Dentro de
ViewNoteFragment(típicamente enonCreate()oonViewCreated()), se recupera elBundlede los argumentos para acceder a los datos de la nota y mostrarlos en la interfaz de usuario.
- Cuando
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 | ||
![]() |
|
|
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
Lo siento, debes estar conectado para publicar un comentario.