Skip to main content

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

1

View Favorites

Access your saved favorites screen to see all images you’ve marked as favorites across all features.
2

Add Comments

Write personal notes or observations for each favorite image. Comments are saved instantly to Firebase.
3

Edit Comments

Update existing comments at any time. Changes sync immediately across devices.
4

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 System

Load Comments

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"
    }
}

Add or Update Comment

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 Node

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

Display Favorites with Comments

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()
                    }
                }
            )
        }
    }
}

Comment Input Field

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.
Displays: “Error al guardar el comentario” with exception details.
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

1

Inject ViewModel

@Composable
fun FavoritesScreen(
    favoritesViewModel: FavoritesViewModel = hiltViewModel()
) {
    // Implementation
}
2

Load Favorites on Launch

LaunchedEffect(Unit) {
    favoritesViewModel.loadFavoriteImages()
}
3

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()
4

Handle Empty State

if (favoriteImages.isEmpty() && !isLoading) {
    EmptyFavoritesView(
        message = errorMessage.ifEmpty { "No favorites yet" }
    )
}
5

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

Performance Optimization

  • 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.

Build docs developers (and LLMs) love