Bird
Raised Fist0
Android Kotlinmobile~20 mins

Dependency injection with Hilt in Android Kotlin - Mini App: Build & Ship

Choose your learning style10 modes available
Build: UserProfileScreen
This screen shows a user's profile information fetched using a repository injected by Hilt.
Target UI
-------------------------
|      User Profile      |
|-----------------------|
| Name: Loading...       |
| Email: Loading...      |
|                       |
| [Refresh]             |
-------------------------
Use Hilt to inject a UserRepository into the ViewModel.
UserRepository provides user data (name and email).
ViewModel fetches user data from UserRepository.
UserProfileScreen observes ViewModel state and displays user info.
Add a Refresh button to reload user data.
Starter Code
Android Kotlin
package com.example.userprofile

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dagger.hilt.android.AndroidEntryPoint
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

// TODO: Add User data class here

// TODO: Add UserRepository interface and implementation here

// TODO: Add ViewModel with injected UserRepository here

@AndroidEntryPoint
class UserProfileActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.padding(16.dp)) {
                    UserProfileScreen()
                }
            }
        }
    }
}

@Composable
fun UserProfileScreen() {
    // TODO: Obtain ViewModel and observe user state
    // TODO: Display user name and email or loading text
    // TODO: Add Refresh button to reload user data
}
Task 1
Task 2
Task 3
Task 4
Task 5
Task 6
Solution
Android Kotlin
package com.example.userprofile

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

// Data class representing a user
data class User(val name: String, val email: String)

// Repository interface
interface UserRepository {
    suspend fun getUser(): User
}

// Fake implementation of UserRepository
class FakeUserRepository @Inject constructor() : UserRepository {
    override suspend fun getUser(): User {
        // Simulate network or database delay
        kotlinx.coroutines.delay(500)
        return User(name = "Alice Johnson", email = "alice@example.com")
    }
}

// ViewModel with injected UserRepository
@HiltViewModel
class UserProfileViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    private val _user = MutableStateFlow<User?>(null)
    val user: StateFlow<User?> = _user

    init {
        loadUser()
    }

    fun loadUser() {
        viewModelScope.launch {
            val fetchedUser = userRepository.getUser()
            _user.value = fetchedUser
        }
    }
}

@AndroidEntryPoint
class UserProfileActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.padding(16.dp)) {
                    UserProfileScreen()
                }
            }
        }
    }
}

import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
    val userState by viewModel.user.collectAsState()

    Column(modifier = Modifier.padding(16.dp)) {
        if (userState == null) {
            Text(text = "Name: Loading...")
            Text(text = "Email: Loading...")
        } else {
            Text(text = "Name: ${userState.name}")
            Text(text = "Email: ${userState.email}")
        }

        Button(onClick = { viewModel.loadUser() }, modifier = Modifier.padding(top = 16.dp)) {
            Text("Refresh")
        }
    }
}

This solution uses Hilt to inject the UserRepository into the UserProfileViewModel. The repository provides user data asynchronously. The ViewModel exposes this data as a StateFlow which the composable observes to update the UI reactively.

The UserProfileScreen composable uses hiltViewModel() to get the ViewModel instance with dependencies injected. It shows loading text while data is null, then displays the user's name and email once loaded. The Refresh button triggers reloading the user data.

This pattern cleanly separates data fetching logic from UI, making the code easier to test and maintain.

Final Result
Completed Screen
-------------------------
|      User Profile      |
|-----------------------|
| Name: Alice Johnson    |
| Email: alice@example.com|
|                       |
| [Refresh]             |
-------------------------
When the screen loads, it shows 'Loading...' text briefly, then displays the user's name and email.
Tapping the Refresh button reloads the user data and updates the displayed info.
Stretch Goal
Add a loading spinner that shows while user data is being fetched.
💡 Hint
Use a Boolean state in ViewModel to track loading status and show a CircularProgressIndicator in the UI when loading.

Practice

(1/5)
1. What is the primary purpose of using @HiltAndroidApp annotation in an Android application?
easy
A. To inject dependencies into a ViewModel
B. To mark an Activity as injectable by Hilt
C. To trigger Hilt's code generation and setup the application-level dependency container
D. To define a module that provides dependencies

Solution

  1. Step 1: Understand the role of @HiltAndroidApp

    This annotation is placed on the Application class to start Hilt's code generation and create the application-level dependency container.
  2. Step 2: Differentiate from other annotations

    @AndroidEntryPoint marks Activities or Fragments, @Module defines providers, and @Inject is for injection points, so they do not setup the application container.
  3. Final Answer:

    To trigger Hilt's code generation and setup the application-level dependency container -> Option C
  4. Quick Check:

    @HiltAndroidApp = Setup app container [OK]
Hint: Remember: @HiltAndroidApp is only on Application class [OK]
Common Mistakes:
  • Confusing @HiltAndroidApp with @AndroidEntryPoint
  • Thinking @HiltAndroidApp is for Activities
  • Using @HiltAndroidApp on modules
2. Which of the following is the correct way to inject a dependency into an Android Activity using Hilt?
easy
A. Annotate the Activity with @AndroidEntryPoint and use @Inject lateinit var for the dependency
B. Use @Module annotation on the Activity and call inject() manually
C. Add @HiltAndroidApp on the Activity and initialize dependencies in onCreate()
D. Declare dependencies as constructor parameters without annotations

Solution

  1. Step 1: Identify how to enable injection in Activity

    Using @AndroidEntryPoint on the Activity allows Hilt to generate necessary code for injection.
  2. Step 2: Inject dependencies inside Activity

    Use @Inject lateinit var to declare dependencies that Hilt will provide automatically.
  3. Final Answer:

    Annotate the Activity with @AndroidEntryPoint and use @Inject lateinit var for the dependency -> Option A
  4. Quick Check:

    @AndroidEntryPoint + @Inject lateinit var = Activity injection [OK]
Hint: Use @AndroidEntryPoint on Activity and @Inject lateinit var inside [OK]
Common Mistakes:
  • Trying to use @Module on Activity
  • Forgetting @AndroidEntryPoint annotation
  • Injecting dependencies via constructor in Activity
3. Given the following Hilt module code, what will be the output when repository.getData() is called?
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    fun provideData(): String = "Hello Hilt"
}

class Repository @Inject constructor(private val data: String) {
    fun getData() = data
}
medium
A. "DataModule"
B. Compilation error due to missing @Inject on Repository
C. NullPointerException at runtime
D. "Hello Hilt"

Solution

  1. Step 1: Understand the provided dependency

    The module provides a String "Hello Hilt" via the provideData() function.
  2. Step 2: Check Repository injection

    Repository constructor is annotated with @Inject and receives the String dependency correctly.
  3. Final Answer:

    "Hello Hilt" -> Option D
  4. Quick Check:

    Provided String = "Hello Hilt" [OK]
Hint: Look for @Provides return value matching injected type [OK]
Common Mistakes:
  • Assuming @Inject missing on Repository causes error (it is present)
  • Confusing module name with provided value
  • Expecting runtime null error without reason
4. Identify the error in the following Hilt setup:
@Module
@InstallIn(ActivityComponent::class)
object NetworkModule {
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder().baseUrl("https://api.example.com").build()
    }
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var retrofit: Retrofit

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}
medium
A. Retrofit cannot be provided in ActivityComponent scope because it should be Singleton
B. Missing @Inject constructor in Retrofit class
C. MainActivity must be annotated with @HiltAndroidApp
D. Retrofit instance must be created manually, not with @Provides

Solution

  1. Step 1: Check the component scope for Retrofit

    Retrofit is usually a singleton because it is expensive to create and shared across app.
  2. Step 2: Analyze the module's @InstallIn scope

    The module is installed in ActivityComponent, which creates a new instance per Activity, not singleton.
  3. Step 3: Identify the mismatch

    Providing Retrofit in ActivityComponent scope causes multiple instances, which is incorrect for Retrofit.
  4. Final Answer:

    Retrofit cannot be provided in ActivityComponent scope because it should be Singleton -> Option A
  5. Quick Check:

    Retrofit = Singleton scope needed [OK]
Hint: Provide expensive objects in SingletonComponent scope [OK]
Common Mistakes:
  • Thinking Retrofit needs @Inject constructor
  • Confusing @HiltAndroidApp with @AndroidEntryPoint
  • Believing manual creation is required with @Provides
5. You want to inject two different implementations of an interface Logger using Hilt in your Android app. Which approach correctly distinguishes these implementations for injection?
hard
A. Provide both implementations in the same module without qualifiers and inject List<Logger>
B. Use @Qualifier annotations with different names on each provider and injection site
C. Use @Singleton on both implementations and inject Logger directly
D. Create separate modules for each implementation without any annotations

Solution

  1. Step 1: Understand multiple implementations injection

    When multiple implementations of the same interface exist, Hilt needs a way to distinguish them.
  2. Step 2: Use @Qualifier annotations

    Custom @Qualifier annotations with different names allow Hilt to know which implementation to inject where.
  3. Step 3: Reject other options

    Injecting List<Logger> is possible but does not distinguish single injection points; @Singleton does not differentiate implementations; separate modules without qualifiers cause ambiguity.
  4. Final Answer:

    Use @Qualifier annotations with different names on each provider and injection site -> Option B
  5. Quick Check:

    Multiple implementations = use @Qualifier [OK]
Hint: Use @Qualifier to name different implementations [OK]
Common Mistakes:
  • Injecting interface without qualifiers causes errors
  • Assuming @Singleton differentiates implementations
  • Not using qualifiers when multiple providers exist
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard