Skip to main content

Overview

NASA Explorer follows the Model-View-ViewModel (MVVM) architectural pattern, which provides clear separation between UI code and business logic. This pattern is the recommended approach for Android apps using Jetpack Compose.

MVVM Components

1

Model

Represents the data and business logic. In NASA Explorer, models are defined in the domain package and represent the core business entities.
2

View

The UI layer built with Jetpack Compose. Views observe state from ViewModels and render the UI accordingly.
3

ViewModel

Acts as a bridge between the View and Model. Manages UI state, handles business logic, and survives configuration changes.

The Model Layer

Models represent the core business entities and are independent of any framework:
domain/NasaModel.kt
package com.ccandeladev.nasaexplorer.domain

// Modelo contiene solo los datos que se usaran en la UI
data class NasaModel(
    val title: String,
    val url: String,
    val date: String,
    val explanation: String,
)
The domain model is decoupled from the API response format. This separation allows the API to change without affecting the rest of the application.
The repository layer transforms API responses into domain models:
data/api/NasaRepository.kt
class NasaRepository @Inject constructor(
    private val nasaApiService: NasaApiService
) {
    companion object {
        private const val API_KEY = BuildConfig.NASA_API_KEY
    }

    // Obtener imagen del día
    suspend fun getImageOfTheDay(date: String? = null): NasaModel {
        val response = nasaApiService.getImageOfTheDay(
            apiKey = API_KEY, 
            date = date
        )
        return response.toNasaModel() // Convierte la respuesta a NasaModel
    }

    // Obtener imágenes en un rango de fechas
    suspend fun getImagesInRange(
        startDate: String, 
        endDate: String? = null
    ): List<NasaModel> {
        val response = nasaApiService.getImagesInRange(
            apiKey = API_KEY,
            startDate = startDate,
            endDate = endDate
        )
        return response.map { it.toNasaModel() }
    }

    // Obtener imágenes aleatorias
    suspend fun getRandomImages(count: Int): List<NasaModel> {
        val response = nasaApiService.getRandomImages(
            apiKey = API_KEY, 
            count = count
        )
        return response.map { it.toNasaModel() }
    }
}

The ViewModel Layer

ViewModels manage UI state and business logic using Kotlin coroutines and StateFlow:
ui/homescreen/HomeScreenViewModel.kt
@HiltViewModel
class HomeScreenViewModel @Inject constructor(
    private val authService: AuthService
) : ViewModel() {

    // Cerrar sesión (Hilo secundario)
    fun logOut(){
        viewModelScope.launch(Dispatchers.IO){
            authService.userLogout()
        }
    }
}
A more complex example with state management:
ui/dailyimagescreen/DailyImageViewModel.kt
@HiltViewModel
class DailyImageViewModel @Inject constructor(
    private val nasaRepository: NasaRepository,
    private val firebaseAuth: FirebaseAuth,
    private val firebaseDatabase: FirebaseDatabase
) : ViewModel() {

    // Almacena la imagen diaria. Inicia en null
    private val _dailyImage = MutableStateFlow<NasaModel?>(null)
    val dailyImage: StateFlow<NasaModel?> = _dailyImage

    // Estado para manejar mensajes de error
    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage

    // Controlar el estado de la carga de la imagen
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading

    // Controlar el estado del icono de favoritos
    private val _isFavorite = MutableStateFlow<Boolean>(false)
    val isFavorite: StateFlow<Boolean> = _isFavorite

    /**
     * Cargar la imagen diaria
     * @param date opcion de cargar imagen en una fecha especificada
     */
    fun loadDailyImage(date: String? = null) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val result = nasaRepository.getImageOfTheDay(date = date)
                _dailyImage.value = result
                _errorMessage.value = null
            } catch (e: Exception) {
                _errorMessage.value = "Sin conexión a internet..."
                _dailyImage.value = null
            } finally {
                _isLoading.value = false
            }
        }
    }

    /**
     * Guarda una imagen como favorita en Firebase
     */
    fun saveToFavorites(nasaModel: NasaModel) {
        val userId = firebaseAuth.currentUser?.uid
        if (userId != null) {
            viewModelScope.launch {
                try {
                    // Crear id en la BD
                    val favoriteRef = firebaseDatabase.reference
                        .child("favorites")
                        .child(userId)
                        .push()
                    
                    val favoriteImage = mapOf(
                        "id" to (favoriteRef.key ?: ""),
                        "title" to nasaModel.title,
                        "url" to nasaModel.url
                    )
                    
                    favoriteRef.setValue(favoriteImage).await()
                    _isFavorite.value = true
                } catch (e: Exception) {
                    _errorMessage.value = "Error al guardar favorito"
                }
            }
        }
    }
}
ViewModels expose immutable StateFlow properties to the UI while keeping mutable MutableStateFlow properties private. This prevents the UI from accidentally modifying state directly.

The View Layer

Views are built with Jetpack Compose and observe ViewModel state:
ui/homescreen/HomeScreen.kt
@Composable
fun HomeScreen(
    homeScreenViewModel: HomeScreenViewModel = hiltViewModel(),
    onNavigateToLogin: () -> Unit
) {
    val navController = rememberNavController()

    // Botón "atrás" -> solo vuelve al home
    BackHandler {
        navController.popBackStack(Routes.Home, inclusive = false)
    }

    Scaffold(
        topBar = {
            HomeTopBar(
                navController = navController,
                homeScreenViewModel = homeScreenViewModel,
                onNavigateToLogin = onNavigateToLogin
            )
        },
        bottomBar = {
            HomeBottomBar(navController = navController)
        }
    ) { paddingValues ->
        // NavHost para gestionar navegacion interna
        NavHost(
            navController = navController,
            startDestination = Routes.Home,
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            composable<Routes.Home> { HomeScreenContent() }
            composable<Routes.DailyImage> { DailyImageScreen() }
            composable<Routes.RandomImage> { RandomImageScreen() }
            composable<Routes.RangeImages> { RangeImagesScreen() }
            composable<Routes.FavoriteImages> { FavoritesScreen() }
        }
    }
}

ViewModel Injection

ViewModels are automatically injected using Hilt’s hiltViewModel() function:
@Composable
fun HomeScreen(
    homeScreenViewModel: HomeScreenViewModel = hiltViewModel(),
    onNavigateToLogin: () -> Unit
) {
    // ViewModel is automatically created and injected
    // All dependencies are resolved by Hilt
}
The @HiltViewModel annotation tells Hilt to generate the necessary code for ViewModel injection. Dependencies are provided through constructor injection.

State Management Best Practices

Use StateFlow for Observable State

private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading

Handle Loading States

fun loadData() {
    viewModelScope.launch {
        _isLoading.value = true
        try {
            // Load data
        } catch (e: Exception) {
            _errorMessage.value = e.message
        } finally {
            _isLoading.value = false
        }
    }
}

Use viewModelScope for Coroutines

fun logOut(){
    viewModelScope.launch(Dispatchers.IO){
        authService.userLogout()
    }
}
viewModelScope automatically cancels coroutines when the ViewModel is cleared, preventing memory leaks.

MVVM Data Flow

Benefits of MVVM

UI logic is separated from business logic, making code more maintainable and testable.
ViewModels survive configuration changes like screen rotations, preserving state without additional code.
ViewModels can be unit tested without requiring Android framework dependencies.
StateFlow provides reactive programming patterns, automatically updating UI when state changes.

Dependency Injection

Learn how ViewModels get their dependencies

Navigation

Understand how ViewModels work with navigation

Build docs developers (and LLMs) love