Overview
The Favorites feature provides a personalized collection where users can save their favorite NASA astronomy images. It includes the ability to add custom comments to each image, creating a personal astronomy journal backed by Firebase Realtime Database.
User Experience
View Favorites
Access your saved favorites screen to see all images you’ve marked as favorites across all features.
Add Comments
Write personal notes or observations for each favorite image. Comments are saved instantly to Firebase.
Edit Comments
Update existing comments at any time. Changes sync immediately across devices.
Remove Favorites
Delete images from your favorites. This also removes associated comments.
All favorites and comments are synced across devices using Firebase Realtime Database, so your collection is available wherever you sign in.
Data Models
FavoriteNasaModel
Unlike regular NasaModel, favorites include a Firebase-generated ID:
app/src/main/java/com/ccandeladev/nasaexplorer/domain/FavoriteNasaModel.kt
data class FavoriteNasaModel (
val firebaseImageId: String , // Unique ID generated by Firebase
val title: String ,
val url: String
)
The firebaseImageId is crucial for associating comments with specific favorite images, even if the same APOD image is favorited multiple times on different dates.
ViewModel Architecture
FavoritesViewModel
Manages favorites list, comments, and all related operations:
app/src/main/java/com/ccandeladev/nasaexplorer/ui/favoritesscreen/FavoritesViewModel.kt
@HiltViewModel
class FavoritesViewModel @Inject constructor (
private val firebaseAuth: FirebaseAuth ,
private val firebaseDatabase: FirebaseDatabase
) : ViewModel () {
private val _favoriteImages = MutableStateFlow < List < FavoriteNasaModel >>( emptyList ())
val favoriteImages: StateFlow < List < FavoriteNasaModel >> = _favoriteImages
private val _errorMessage = MutableStateFlow < String >( "" )
val errorMessage: StateFlow < String > = _errorMessage
private val _isLoading = MutableStateFlow < Boolean >( false )
val isLoading: StateFlow < Boolean > = _isLoading
private val _comments = MutableStateFlow < Map < String , String >>( emptyMap ())
val comments: StateFlow < Map < String , String >> = _comments
}
State Management
favoriteImages List of FavoriteNasaModel containing all user’s saved images.
isLoading Boolean indicating active Firebase operations.
errorMessage Error messages for display to user.
comments Map of Firebase image IDs to comment strings.
Loading Favorites
Load Favorite Images
app/src/main/java/com/ccandeladev/nasaexplorer/ui/favoritesscreen/FavoritesViewModel.kt
fun loadFavoriteImages () {
val userId = firebaseAuth.currentUser?.uid
if (userId != null ) {
viewModelScope. launch {
_isLoading. value = true
try {
val favoriteRef = firebaseDatabase.reference
. child ( "favorites" )
. child (userId)
val snapshot = favoriteRef. get (). await ()
val favoriteList = snapshot.children. mapNotNull { child ->
val firebaseImageId = child.key ?: return @mapNotNull null
val title = child. child ( "title" ). getValue (String:: class .java)
val url = child. child ( "url" ). getValue (String:: class .java)
if (title != null && url != null ) {
FavoriteNasaModel (
firebaseImageId = firebaseImageId,
title = title,
url = url
)
} else null
}
if (favoriteList. isEmpty ()) {
_errorMessage. value = "No tienes imágenes favoritas"
}
_favoriteImages. value = favoriteList
// Load comments after images are loaded
loadComments ()
} catch (e: Exception ) {
_errorMessage. value = "Error al mostrar la lista de favoritos"
_favoriteImages. value = emptyList ()
} finally {
_isLoading. value = false
}
}
} else {
_errorMessage. value = "Usuario no autenticado"
}
}
The function automatically loads comments after fetching favorites, ensuring all data is ready for display.
Comments are loaded separately and mapped to favorite images:
app/src/main/java/com/ccandeladev/nasaexplorer/ui/favoritesscreen/FavoritesViewModel.kt
private fun loadComments () {
val userId = firebaseAuth.currentUser?.uid
if (userId != null ) {
viewModelScope. launch {
try {
val commentsRef = firebaseDatabase.reference
. child ( "comments" )
. child (userId)
val snapshot = commentsRef. get (). await ()
val commentsMap = snapshot.children. associate { child ->
val favoriteImageId = child.key ?: ""
val comment = child. getValue (String:: class .java) ?: ""
favoriteImageId to comment
}
_comments. value = commentsMap
} catch (e: Exception ) {
_errorMessage. value = "Error al cargar comentario: ${ e.message } "
}
}
} else {
_errorMessage. value = "Usuario no autenticado"
}
}
app/src/main/java/com/ccandeladev/nasaexplorer/ui/favoritesscreen/FavoritesViewModel.kt
fun addComment (firebaseImageId: String , comment: String ) {
val userId = firebaseAuth.currentUser?.uid
if (userId != null ) {
viewModelScope. launch {
try {
val commentsRef = firebaseDatabase.reference
. child ( "comments" )
. child (userId)
commentsRef. child (firebaseImageId). setValue (comment). await ()
// Update local state
val currentComment = _comments. value . toMutableMap ()
currentComment[firebaseImageId] = comment
_comments. value = currentComment
} catch (e: Exception ) {
_errorMessage. value = "Error al guardar el comentario: ${ e.message } "
}
}
} else {
_errorMessage. value = "Usuario no autenticado"
}
}
The addComment function handles both adding new comments and updating existing ones. Firebase automatically overwrites the value if a comment already exists for that image ID.
Remove from Favorites
Deleting a favorite also removes its associated comment:
app/src/main/java/com/ccandeladev/nasaexplorer/ui/favoritesscreen/FavoritesViewModel.kt
fun removeFromFavorites (favoriteNasaModel: FavoriteNasaModel , onRemoved: () -> Unit) {
val userId = firebaseAuth.currentUser?.uid
if (userId != null ) {
viewModelScope. launch {
try {
val favoriteRef = firebaseDatabase.reference
. child ( "favorites" )
. child (userId)
// Remove favorite image
val snapshot = favoriteRef
. orderByChild ( "id" )
. equalTo (favoriteNasaModel.firebaseImageId)
. get ()
. await ()
for (child in snapshot.children) {
child.ref. removeValue (). await ()
}
// Remove associated comment
val commentRef = firebaseDatabase.reference
. child ( "comments" )
. child (userId)
commentRef. child (favoriteNasaModel.firebaseImageId)
. removeValue ()
. await ()
// Callback after successful removal
onRemoved ()
} catch (e: Exception ) {
_errorMessage. value = "Error al eliminar favoritos ${ e.message } "
}
}
} else {
_errorMessage. value = "Usuario no autenticado"
}
}
The onRemoved callback should trigger a refresh of the favorites list in the UI to reflect the deletion.
Firebase Database Structure
Favorites Node
favorites/
└── {userId}/
├── {firebaseImageId1}/
│ ├── id: "{firebaseImageId1}"
│ ├── title: "Orion Nebula"
│ └── url: "https://apod.nasa.gov/apod/image/..."
└── {firebaseImageId2}/
├── id: "{firebaseImageId2}"
├── title: "Andromeda Galaxy"
└── url: "https://apod.nasa.gov/apod/image/..."
comments/
└── {userId}/
├── {firebaseImageId1}: "Beautiful nebula! My favorite deep space object."
└── {firebaseImageId2}: "Stunning view of our neighboring galaxy."
Comments are stored separately from favorites, allowing for efficient updates without modifying the favorite image data.
UI Implementation
val favoriteImages by viewModel.favoriteImages. collectAsState ()
val comments by viewModel.comments. collectAsState ()
val isLoading by viewModel.isLoading. collectAsState ()
if (isLoading) {
CircularProgressIndicator ()
} else {
LazyColumn {
items (favoriteImages) { favorite ->
val imageComment = comments[favorite.firebaseImageId] ?: ""
FavoriteImageCard (
favoriteNasaModel = favorite,
comment = imageComment,
onCommentChange = { newComment ->
viewModel. addComment (favorite.firebaseImageId, newComment)
},
onRemove = {
viewModel. removeFromFavorites (favorite) {
// Refresh the list after removal
viewModel. loadFavoriteImages ()
}
}
)
}
}
}
var commentText by remember { mutableStateOf (initialComment) }
OutlinedTextField (
value = commentText,
onValueChange = { commentText = it },
label = { Text ( "Add a comment" ) },
modifier = Modifier. fillMaxWidth ()
)
Button (
onClick = {
viewModel. addComment (favorite.firebaseImageId, commentText)
}
) {
Text ( "Save Comment" )
}
Error Handling
Displays: “No tienes imágenes favoritas” when the favorites list is empty.
Shows: “Error al mostrar la lista de favoritos” for Firebase read failures.
Shows: “Error al eliminar favoritos” if removal operation fails.
All operations check for authenticated user and show: “Usuario no autenticado” if not logged in.
Developer Guide
Inject ViewModel
@Composable
fun FavoritesScreen (
favoritesViewModel: FavoritesViewModel = hiltViewModel ()
) {
// Implementation
}
Load Favorites on Launch
LaunchedEffect (Unit) {
favoritesViewModel. loadFavoriteImages ()
}
Collect States
val favoriteImages by favoritesViewModel.favoriteImages. collectAsState ()
val comments by favoritesViewModel.comments. collectAsState ()
val isLoading by favoritesViewModel.isLoading. collectAsState ()
val errorMessage by favoritesViewModel.errorMessage. collectAsState ()
Handle Empty State
if (favoriteImages. isEmpty () && ! isLoading) {
EmptyFavoritesView (
message = errorMessage. ifEmpty { "No favorites yet" }
)
}
Implement Remove Callback
viewModel. removeFromFavorites (favorite) {
// Refresh the list or navigate away
viewModel. loadFavoriteImages ()
}
Key Features
Cross-Device Sync Firebase automatically syncs favorites and comments across all devices where the user is logged in.
Real-time Updates Changes to comments or favorites are immediately reflected in the UI through StateFlow.
Persistent Storage All data persists in Firebase Realtime Database, surviving app uninstalls.
Personal Journal Comments transform favorites into a personal astronomy journal.
Best Practices
Always provide visual feedback during Firebase operations using the isLoading state, as network latency can vary.
Recommended Practices:
Show loading indicators during Firebase operations
Display empty states with helpful messages
Implement optimistic UI updates for better perceived performance
Handle authentication state changes gracefully
Provide undo functionality for accidental deletions
Comments and favorites are loaded in parallel after initial fetch
State updates use immutable map operations for efficient reactivity
Firebase queries use indexing on child values for fast lookups
Separate storage of comments allows updating text without touching image data
The favorites list loads once per screen visit. For real-time updates from other devices, consider implementing Firebase listeners instead of one-time reads.