5.3.4 CS-Android: Exemple ampliat, combinant amb Cloud Firestore
Anem a fer ara un exemple complet, d'una cosa que serà molt més real que fins ara: combinar Cloud Firestore amb Cloud Storage:
- En Cloud Storage ens guardem les dades grans, com poden ser imatges, audios, vídeos, ...
- En Cloud Firestore ens guardem totes les altres dades, i inclourem el nom de l'arxiu que hem guardat en Cloud Firestore
Per tant hem de combinar els accessos a les dues parts de Firebase, i haurem de comptar sobretot amb que les dades, tant en un com en l'altre, s'agafen de forma asíncrona, i per tant hem de cuidar de no utilitzar unes dades que potser encara no estan disponibles.
El més correcte seria utilitzar corrutines, que juguen amb aquesta forma paral·lela d'executar les coses.
Però per senzillesa començarem sense les corrutines, i senzillament haurem d'estar segurs de que les dades estan ja disponibles quan les utilitzem.
Partirem del mateix exemple utilitzat en l'Annex d'Android, CoffeeShops_Fragment, i ara adequarem les dades a l'estructura de Firebase CloudFirestore, recordant que les imatges dels cafès estaran guardades en Cloud Storage. Aquesta és l'estructura de les dades:
![]() |
![]() |
Com veiem tenim una col·lecció anomenada CoffeeShops amb un document per cada cafè.
Dins del document del cafè tenim nom, adreca, punts i també el nom de la imatge guardada Cloud Storage. També tenim una col·lecció dins del document anomenada comentaris, i en cada document d'aquesta col·lecció de moment només tenim una parella clau-valor: comentari
En la següent imatge es veuen els fitxers guardats en Cloud Storage. Per a que estiga un poc més organitzat, les hem deixades en la carpeta CoffeeShops :

Com hem dit partirem de l'exemple ja fet CoffeeShops_Fragments, no de CoffeeShops_Fragments_ROOM, perquè això ens obligaria a llevar moltes coses. Fins i tot per a que no es quede el nom anterior, potser siga convenient començar de zero amb un projecte nou.
Si és així:
- Creeu un nou projecte anomenat CoffeeShops_Fragments_FIREBASE
- Connecteu amb Cloud Firestore i Cloud Storage, utilitzant els assistents. Connectar l'aplicació només us farà falta en la primera ocasió, però incorporar llibreries, serà en cadascun dels casos.
- Incorporeu la línia següent en el build.gradle de l'apliació, dins de android i dins de defaultConfig, per a que Cloud Firestore no done error:
multiDexEnabled true
- També incorporarem en el build.gradle de l'aplicació les llibreries que ens permeten la navegació entre fragments. Han d'anar en la zona de dependencies
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
- Copieu el següent contingut al fitxer strings.xml, que està dins de res -> values. En ell s'estan definint cadenes que s'utilitzaran en diferents mòduls
<resources>
<string name="app_name">CoffeeShops_Fragments_FIREBASE</string>
<string name="action_settings">Settings</string>
<!-- Strings used for fragments for navigation -->
<string name="first_fragment_label">First Fragment</string>
<string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="reserve">Reserve</string>
<string name="hello_first_fragment">Hello first fragment</string>
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
</resources>
- També ens farà falta el fitxer del menú menu_main.xml, que ha d'estar en res -> menu
<?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"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>
- I per últim copieu el següent contingut al fitxer themes.xml, que està en res -> values -> themes, i on es defineixen uns estils utilitzats:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.CoffeeShops_Fragments_FIREBASE" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
<style name="Theme.CoffeeShops_Fragments_FIREBASE.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.CoffeeShops_Fragments_FIREBASE.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.CoffeeShops_Fragments_FIREBASE.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
Classe Coffee
Per l'estructura de les dades, en què tenim guardat nom, adreça, punts i imatge (no ens fa falta per exemple el número de cafè ja que no ens fa falta una clau principal, com era el cas de Bases de Dades a travès de Room), i en concordància amb els components definits en l'aplicació anterior de CoffeeShops_Fragments, podem definir la classe Coffee d'aquesta manera:
import java.io.Serializable
data class Coffee (val title: String?, val subtitle: String?,
val points: Int?, var image: ByteArray? ): Serializable
- La raó de definir totes les propietats com a val excepte la imatge que serà var és perquè la imatge la modificarem amb posterioritat
- Ho definim com a Serializable per a poder passar tot l'objecte del primer fragment al segon, així si s'ha de visualitzar una altra vegada la imatge, ja la tenim disponible. Si només s'ha de visualitzar el nom del cafè, a banda dels seus comentaris, no faria falta passar tot l'objecte, només el nom.
El layout associat serà item_coffee.xml, amb aquest aspecte:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card1"
android:layout_marginBottom="16dp"
android:layout_marginRight="16dp"
android:layout_marginLeft="16dp"
android:layout_width="match_parent"
android:layout_height="450dp"
card_view:cardCornerRadius="8dp"
card_view:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="200dp"
android:layout_marginLeft="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="25sp"
android:textStyle="bold"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<RatingBar
android:id="@+id/ratingBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:max="5"
android:numStars="5"
android:scaleX="0.5"
android:scaleY="0.5"
android:transformPivotX="0dp"
android:transformPivotY="0dp" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="130dp"
android:text="0" />
</FrameLayout>
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="18sp"
android:layout_marginBottom="16dp"/>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="16dp"
android:background="?android:attr/listDivider" />
<Button
android:id="@+id/BtnReserve"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reserve"
android:textSize="18sp"
android:layout_marginTop="16dp"
style="?android:attr/buttonBarButtonStyle" />
</LinearLayout>
<ImageView
android:id="@+id/img1"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop" />
</androidx.cardview.widget.CardView>
El seu adapter, CoffeeAdapter, quedaria així:
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RatingBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class CoffeeAdapter(private val items: ArrayList<Coffee>) : RecyclerView.Adapter<CoffeeAdapter.CoffeeViewHolder>() {
lateinit var onClick: (View) -> Unit
class CoffeeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val image: ImageView
private val text: TextView
private val text1: TextView
private val barstars: RatingBar
private val points: TextView
init {
image = itemView.findViewById(R.id.img1)
text = itemView.findViewById(R.id.textView)
text1 = itemView.findViewById(R.id.textView1)
points = itemView.findViewById(R.id.textView2)
barstars = itemView.findViewById(R.id.ratingBar)
}
fun bindCards(t: Coffee, onClick: (View) -> Unit) {
//image.setImageResource(t.image)
val img = t.image
if (img != null) {
val imgBmp = BitmapFactory.decodeByteArray(img, 0, img.size)
image.setImageBitmap(imgBmp)
}
text.text = t.title
text1.text = t.subtitle
if (t.points!=null) {
barstars.rating = t.points.toFloat()
points.text=t.points.toString()
}
barstars.onRatingBarChangeListener = RatingBar.OnRatingBarChangeListener { ratingBar: RatingBar, fl: Float, b: Boolean ->
points.text = fl.toString()
}
itemView.setOnClickListener{ onClick(itemView) }
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): CoffeeViewHolder {
val itemView = LayoutInflater.from(viewGroup.context).inflate(R.layout.item_coffee, viewGroup, false)
return CoffeeViewHolder(
itemView
)
}
override fun onBindViewHolder(viewHolder: CoffeeViewHolder, pos: Int) {
val item = items[pos]
viewHolder.bindCards(item, onClick)
}
override fun getItemCount(): Int {
return items.size
}
override fun getItemViewType(position: Int): Int {
return position
}
}
Classe Comment
El comentari de moment el tenim molt senzill. Segurament estaria bé gaurdar més coses, com la data del comentari i l'autor (a banda de valoracions al comentari). Seria tan senzill com fer aquesta classe més completa.
Però de moment només guardem el propi comentari, per tant la classe Comment queda així de senzilla:
data class Comment (val comm: String?)
El layout associat serà item_comment.xml, amb aquest aspecte:
<?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="100dp"
app:cardElevation="6dp"
app:cardCornerRadius="8dp"
android:layout_margin="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/cita"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="TextView" />
</LinearLayout>
</androidx.cardview.widget.CardView>
I el seu adapter, CommentsAdapter, així:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class CommentsAdapter(private val items: ArrayList<Comment>) : RecyclerView.Adapter<CommentsAdapter.CoffeeViewHolder>() {
class CoffeeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val texto: TextView
init {
texto = itemView.findViewById(R.id.cita)
}
fun bindComment(c: Comment) {
texto.text = c.comm
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): CoffeeViewHolder {
val itemView = LayoutInflater.from(viewGroup.context).inflate(R.layout.item_comment, viewGroup, false)
return CoffeeViewHolder(
itemView
)
}
override fun onBindViewHolder(viewHolder: CoffeeViewHolder, pos: Int) {
val item = items[pos]
viewHolder.bindComment(item)
}
override fun getItemCount(): Int {
return items.size
}
}
FirstFragment
En aquesta aplicació de fragments, ballàvem entre 2 fragments. En el primer veiem les dades dels cafès: nom, adreça, puntuació i imatge. Els comentaris els deixàvem per al segon fragment.
El seu layout és fragment_first.xml, i té aquest aspecte:
<?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=".FirstFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recView"
android:layout_marginTop="5dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
I el programa corresponent al primer fragment, FirstFragment, és on tenim més feina:
- Ens connectem a CloudFirestore per a agafar tots els documents de la col·lecció CoffeeShops per mig d'un addSnapshotListener, que buscarà tots els documents de la col·lecció (línia 39)
- Construïm l'objecte Coffee, però sense haver agafat encara la imatge (per això havíem definit la propietat com a var), perquè com haurem de buscar-la a Cloud Storage, en el moment de construir l'objecte no la tindríem. Per tant de moment no tenim res en la propietat image de l'objecte (línies 47 a 50)
- L'objecte Coffee l'afegim a l'ArrayList<Coffee> anomenat items (línia 47)
- En un altre ArrayList<String> anomenat itemsDocs ens guardarem el nom del document. Això és per fer més senzilla la recerca del document amb posterioritat (quan busquem els seus comentaris) (línia 51)
- Llancem la recerca de la imatge en Cloud Storage (línia 58). Observeu que en el moment d'agafar la referència, en el nom de la imatge hem incorporat la carpeta CoffeeShops (línia 56)
- Quan estiga disponible l'assignarem al item corresponent (línia 61)
- Ens hem d'assegurar que quan anem a crear l'adaptador CoffeeAdaptor, tots els items estiguen creats. Per això en muntem un addOnSuccessListener també sobre la col·lecció. Firebase ens assegura que s'activarà després del addOnSnapshotListener, per tant és una bona manera d'assegurar-nos que ja estan creats tots els items (faltarà per omplir les imatges, però els items amb els objectes Coffee ja estaran creats) (línia 72 i següents)
- En el onClick de l'adaptador, cridarem al segon fragment, passant 2 paràmetres per mig del bundle (línia 83):
- L'objecte Coffee del qual volem els comentaris. Podríem passar només el nom del cafè, però d'aquesta manera si per exemple es vol visualitzar la imatge, es pot fer. És per a poder passar tot l'objecte Coffee que declarem la classe com a Serializable
- Passem també el nom del document corresponent a aquest cafè, per a poder llegir la seua col·lecció de comentaris més fàcilment (línia 81)
package com.example.coffeeshops_fragments_firebase
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.gms.tasks.OnFailureListener
import com.google.android.gms.tasks.OnSuccessListener
import com.google.firebase.firestore.DocumentChange
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageReference
/**
* A simple [Fragment] subclass as the default destination in the navigation.
*/
class FirstFragment : Fragment() {
private var items: ArrayList<Coffee> = ArrayList()
private var itemsDocs: ArrayList<String> = ArrayList()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_first, container, false)
val db: FirebaseFirestore = FirebaseFirestore.getInstance()
if (items.size==0) { // per a no executar-lo una altra vegada quan tornem del SecondFragment
val storageRef: StorageReference;
storageRef = FirebaseStorage.getInstance().getReference();
db.collection("CoffeeShops").addSnapshotListener { snapshots, e ->
for (dc in snapshots!!.documentChanges) {
when (dc.type) {
DocumentChange.Type.ADDED -> {
var p = 0
if (dc.document.getString("punts")!="")
p = dc.document.get("punts").toString().toInt()
items!!.add(Coffee(dc.document.getString("nom").toString(),
dc.document.getString("adreca").toString(),
p, null
))
itemsDocs.add(dc.document.id)
val item = items[items.size-1]
val nomImatge = dc.document.getString("imatge")
if (nomImatge != null) {
val imatgeRef = storageRef.child("CoffeeShops/" + nomImatge)
val ONE_MEGABYTE = (1024 * 1024).toLong()
imatgeRef.getBytes(ONE_MEGABYTE)
.addOnSuccessListener(OnSuccessListener<ByteArray> {
// Data for "images/island.jpg" is returns, use this as needed
item.image=it
}).addOnFailureListener(OnFailureListener {
// Handle any errors
})
}
}
}
}
}
}
db.collection("CoffeeShops").get().addOnSuccessListener {
val recView: RecyclerView = root.findViewById(R.id.recView)
recView.setHasFixedSize(true)
val adaptador = CoffeeAdapter(items)
recView.adapter = adaptador
recView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adaptador.onClick = {
val c = items[recView.getChildAdapterPosition(it)]
val d = itemsDocs[recView.getChildAdapterPosition(it)]
val bundle = bundleOf("coffee" to c,"doc" to d)
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment, bundle)
}
}
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
De moment marcarà un error en la línia 83, perquè no està definida la navegació (la incorporarem al final)
Second Fragment
L'utilitzem per a visualitzar els comentaris del cafè seleccionat (sobre qui es fa un click).
El layout associat és fragment_second.xml, i té aquest aspecte:
<?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=".SecondFragment">
<TextView
android:id="@+id/textview_second"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="56dp"
android:gravity="center"
android:text="Segundo Fragment"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_coment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textview_second"
app:layout_constraintVertical_bias="0.158" />
</androidx.constraintlayout.widget.ConstraintLayout>
A aquest fragment, SecondFragment, recordem que li passem 2 paràmetres:
- L'objecte Coffee del qual volem els comentaris.
- El nom del document corresponent a aquest cafè
El funcionament és el següent:
- Agafem els paràmetres passats en coffee i nameDoc (línies 30 i 31)
- Sobre la col·lecció comentaris del document (per això ha estat còmode passar-li el nom del document) muntem un addSnapshotListener, per a obtindre tots els documents de la col·lecció (línia 33)
- És de ressaltar que com tal i com ho fem, no podem assegurar l'ordre en què vindran aquestos documents, i per tant quin serà l'ordre dels comentaris. El més lògic seria ordenar-los cronològicament. Per a això podríem afegir al document del comentari un camp amb la data-hora. Aleshores afegiríem a aquesta sentència de la línia 33 una clàusula d'ordenació; orderBy("date"), si s'anomenara date aquest camp amb la data
- Anem afegint els comentaris a un ArrayList<Comment> anomenat Comments
- En el moment de crear l'adaptador dels comentaris hem d'estar segurs que ja tenim disponibles tots els comentaris en Comments. Amb aquesta finalitat muntem un addOnSuccessListener també sobre la col·lecció comentaris. Firebase ens assegura que aquest es produirà després del addSnapshotListener i per tant ja tindrem l'ArrayList ple (línia 45)
I aquest és el SecondFragment:
package com.example.coffeeshops_fragments_firebase
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.firestore.DocumentChange
import com.google.firebase.firestore.FirebaseFirestore
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
class SecondFragment : Fragment() {
private var Comments = arrayListOf<Comment>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val db: FirebaseFirestore = FirebaseFirestore.getInstance()
val root = inflater.inflate(R.layout.fragment_second, container, false)
val texto: TextView = root.findViewById(R.id.textview_second)
val coffee = arguments?.get("coffee") as Coffee
val nameDoc = arguments?.get("doc") as String
texto.text = coffee.title
db.collection("CoffeeShops").document(nameDoc).collection("comentaris").addSnapshotListener { snapshots, e ->
for (dc in snapshots!!.documentChanges) {
when (dc.type) {
DocumentChange.Type.ADDED -> {
Comments.add(Comment(dc.document.getString("comentari")))
}
}
}
}
val recView: RecyclerView = root.findViewById(R.id.recyclerview_coment)
recView.setHasFixedSize(true)
db.collection("CoffeeShops").document(nameDoc).collection("comentari").
get().addOnSuccessListener {
val adaptador = CommentsAdapter(Comments)
recView.adapter = adaptador
recView.layoutManager = GridLayoutManager(context, 2)
}
val adaptador = CommentsAdapter(Comments)
recView.adapter = adaptador
recView.layoutManager = GridLayoutManager(context, 2)
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/* view.findViewById<Button>(R.id.button_second).setOnClickListener {
findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment)
}*/
}
}
Main Activity i navegació
El MainActivity no ofereix cap dificultat:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
}
Maracrà uns errors, perquè redefinirem activity_main.xml amb alguns elements que falten
I en el seu layout, activity_main.xml, només hem d'observar que en ell inclourem un altre layout en la penúltima línia, el content_main:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.CoffeeShops_Fragments_FIREBASE.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.CoffeeShops_Fragments_FIREBASE.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Marcarà error en la penúltima fila, perquè ens falta definir el content_main.xml
En el mencionat content_main.xml només assenyalem que utilitzarem el navigation/nav_graph:
<?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"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
I és en el nav_graph.xml on es dissenya la manera de navegar. S'ha de crear sobre res -> New -> Android Resource File, amb el nom especificat (no cal posar .xml) i cuidant que siga en recurs de tipus Navigation

I aquest ha de ser el seu contingut
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/nav_graph"
app:startDestination="@id/FirstFragment">
<fragment
android:id="@+id/FirstFragment"
android:name="com.example.coffeeshops_fragments_firebase.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.example.coffeeshops_fragments_firebase.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>
En el moment de crear aquest fitxer, haurien de desaparèixer tots els errors
Si tot ha anat bé, es veurà com en executar, ràpidament apareixen les caixes de cada cafeteria, però potser encara no aparega la imatge. A poc a poc aniran apareixent. I si s'apreta a alguna cafeteria,anirem a veure els comentaris en el segon fragment
![]() |
![]() |
Llicenciat sota la Llicència Creative Commons Reconeixement NoComercial SenseObraDerivada 4.0



