Skip to main content

Overview

NASA Explorer uses Hilt for dependency injection, providing a standardized way to manage dependencies across the application. Hilt is built on top of Dagger and provides compile-time correctness, runtime performance, and Android framework integration.
Hilt is Google’s recommended dependency injection solution for Android. It reduces boilerplate code and provides a consistent way to inject dependencies into Android components.

Why Dependency Injection?

Testability

Easy to swap implementations for testing with mocks

Reusability

Share single instances across the entire app

Maintainability

Centralize dependency creation and configuration

Loose Coupling

Components don’t need to know how to create their dependencies

Project Setup

Hilt is configured in the app’s build configuration:
build.gradle.kts
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.jetbrains.kotlin.android)
    kotlin("kapt")  // Required for Hilt annotation processing
    alias(libs.plugins.hilt)
}

dependencies {
    // Hilt dependencies
    implementation(libs.dagger.hilt)
    implementation(libs.dagger.hilt.navigation)  // Hilt integration with Compose Navigation
    kapt(libs.dagger.hilt.compiler)
}
build.gradle.kts (root)
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.jetbrains.kotlin.android) apply false
    alias(libs.plugins.hilt) apply false
}

Application Class

The entry point for Hilt is the application class annotated with @HiltAndroidApp:
NasaExplorerApp.kt
package com.ccandeladev.nasaexplorer

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class NasaExplorerApp: Application()
The application class must be registered in AndroidManifest.xml with android:name=".NasaExplorerApp"

Activity Integration

Activities that use Hilt must be annotated with @AndroidEntryPoint:
MainActivity.kt
package com.ccandeladev.nasaexplorer

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            NASAExplorerTheme {
                // UI content
            }
        }
    }
}

Hilt Modules

Modules define how to provide dependencies. NASA Explorer has several modules:

API Network Module

Provides Retrofit and HTTP client instances:
ApiNetworkModule.kt
package com.ccandeladev.nasaexplorer.data.di

import com.ccandeladev.nasaexplorer.data.api.NasaApiService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ApiNetworkModule {

    private const val BASE_URL = "https://api.nasa.gov/"

    @Singleton
    @Provides
    fun provideNasaApiService(retrofit: Retrofit): NasaApiService {
        return retrofit.create(NasaApiService::class.java)
    }

    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Singleton
    @Provides
    fun provideHttpClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
}
@InstallIn(SingletonComponent::class) means these dependencies live as long as the application and are shared across all components.

Firebase Module

Provides Firebase instances:
FirebaseModule.kt
package com.ccandeladev.nasaexplorer.data.di

import com.google.firebase.database.FirebaseDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object FirebaseModule {

    @Provides
    @Singleton
    fun provideFirebaseDatabase(): FirebaseDatabase {
        return FirebaseDatabase.getInstance()
    }
}

Auth Network Module

Provides Firebase Authentication:
AuthNetworkModule.kt
package com.ccandeladev.nasaexplorer.data.auth

import com.google.firebase.auth.FirebaseAuth
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AuthNetworkModule {
    @Singleton
    @Provides
    fun provideFirebaseAuth() = FirebaseAuth.getInstance()
}

Constructor Injection

Classes can use @Inject constructor annotation to receive dependencies:

Repository Example

NasaRepository.kt
package com.ccandeladev.nasaexplorer.data.api

import com.ccandeladev.nasaexplorer.BuildConfig
import com.ccandeladev.nasaexplorer.domain.NasaModel
import javax.inject.Inject

class NasaRepository @Inject constructor(
    private val nasaApiService: NasaApiService
) {
    companion object {
        private const val API_KEY = BuildConfig.NASA_API_KEY
    }

    suspend fun getImageOfTheDay(date: String? = null): NasaModel {
        val response = nasaApiService.getImageOfTheDay(apiKey = API_KEY, date = date)
        return response.toNasaModel()
    }

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

Auth Service Example

AuthService.kt
package com.ccandeladev.nasaexplorer.data.auth

import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import kotlinx.coroutines.tasks.await
import javax.inject.Inject

class AuthService @Inject constructor(
    private val firebaseAuth: FirebaseAuth
) {
    suspend fun login(email: String, password: String): FirebaseUser? {
        return firebaseAuth.signInWithEmailAndPassword(email, password).await().user
    }

    suspend fun register(email: String, password: String): FirebaseUser? {
        return firebaseAuth.createUserWithEmailAndPassword(email, password).await().user
    }

    fun userLogout() {
        firebaseAuth.signOut()
    }

    fun isUserLogged(): Boolean {
        return firebaseAuth.currentUser != null
    }
}

ViewModel Injection

ViewModels use @HiltViewModel annotation and constructor injection:
DailyImageViewModel.kt
package com.ccandeladev.nasaexplorer.ui.dailyimagescreen

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ccandeladev.nasaexplorer.data.api.NasaRepository
import com.ccandeladev.nasaexplorer.domain.NasaModel
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.FirebaseDatabase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class DailyImageViewModel @Inject constructor(
    private val nasaRepository: NasaRepository,
    private val firebaseAuth: FirebaseAuth,
    private val firebaseDatabase: FirebaseDatabase
) : ViewModel() {

    private val _dailyImage = MutableStateFlow<NasaModel?>(null)
    val dailyImage: StateFlow<NasaModel?> = _dailyImage

    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage

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

    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 = "Error: ${e.message}"
                _dailyImage.value = null
            } finally {
                _isLoading.value = false
            }
        }
    }
}

Injecting into Composables

Use hiltViewModel() to inject ViewModels into Composable functions:
DailyImageScreen.kt
import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun DailyImageScreen(
    dailyImageViewModel: DailyImageViewModel = hiltViewModel()
) {
    LaunchedEffect(Unit) {
        dailyImageViewModel.loadDailyImage()
    }

    val dailyImage by dailyImageViewModel.dailyImage.collectAsState()
    val isLoading by dailyImageViewModel.isLoading.collectAsState()

    // UI code...
}
HomeScreen.kt
@Composable
fun HomeScreen(
    homeScreenViewModel: HomeScreenViewModel = hiltViewModel(),
    onNavigateToLogin: () -> Unit
) {
    val navController = rememberNavController()
    
    Scaffold(
        topBar = {
            HomeTopBar(
                navController = navController,
                homeScreenViewModel = homeScreenViewModel,
                onNavigateToLogin = onNavigateToLogin
            )
        }
    ) { paddingValues ->
        // Content
    }
}

Dependency Graph Flow

Here’s how dependencies flow through NASA Explorer:

Scopes in Hilt

Lives for the entire application lifetime. Used for repositories, API services, and Firebase instances in NASA Explorer.
Lives as long as the activity. Not used in NASA Explorer since it uses a single activity architecture.
Lives as long as the ViewModel. Automatically provided by Hilt for ViewModels.

Benefits in NASA Explorer

1

Centralized Configuration

All network and Firebase setup is in one place (Hilt modules), making it easy to modify or swap implementations.
2

Automatic Lifecycle Management

Hilt handles creation and destruction of dependencies based on their scope, preventing memory leaks.
3

Compile-Time Safety

Hilt validates dependency graphs at compile time, catching errors before runtime.
4

Easy Testing

Dependencies can be easily swapped with test doubles using Hilt’s testing APIs.

Best Practices

Use Constructor Injection

Prefer constructor injection with @Inject over field injection for better testability

Singleton for Expensive Objects

Use @Singleton scope for expensive objects like Retrofit, OkHttpClient, and Firebase instances

Module Organization

Group related providers into logical modules (API, Auth, Database)

Avoid Over-Injection

Don’t inject simple objects or primitives - only inject complex dependencies

Troubleshooting

Common Issues:
  • Missing @AndroidEntryPoint on Activity causes crashes
  • Forgetting to add application class to AndroidManifest.xml
  • Circular dependencies between injected classes
  • Missing kapt plugin for annotation processing

Resources

Hilt Documentation

Official Hilt documentation from Google

Android Hilt Guide

Android-specific Hilt integration guide

Hilt Testing

Learn how to test with Hilt

Hilt CodeLab

Hands-on codelab for learning Hilt

Build docs developers (and LLMs) love