Hilt๋ก ์์กด์ฑ ์ฃผ์ ํ๊ธฐ
์ง๊ธ TodoViewModel ํ๋๋ฅผ ์ด์ด๋ณด๋ฉด ์ด๋ ๋ค: Room ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ง์ ๋ง๋ค๊ณ , DataStore๋ ์ง์ ๋ง๋ค๊ณ , Retrofit๋ ์ง์ ๋ง๋ ๋ค. ํ ํด๋์ค๊ฐ ํ๋ ์ผ์ด ๋๋ฌด ๋ง๋ค.
์ง๊ธ ์ฝ๋์ ๋ฌธ์
class TodoViewModel(application: Application) : AndroidViewModel(application) {
private val dao = Room.databaseBuilder(application, TodoDatabase::class.java, "todo.db")
.build()
.todoDao()
private val settingsRepository = SettingsRepository(application.settingsDataStore)
private val todoApiService = Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TodoApiService::class.java)
// ... addTodo, toggleDone, deleteTodo, importFromServer, toggleDarkMode ...
}TodoViewModel์ ํ
์คํธํ๋ ค๋ฉด ์ง์ง Room ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ง์ง ๋คํธ์ํฌ ์ฐ๊ฒฐ์ด ํ์ํ๋ค. ๊ฐ์ง(mock) ๋ฐ์ดํฐ๋ก ๋ฐ๊ฟ์ ํ
์คํธํ๊ธฐ๊ฐ ์ด๋ ต๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ์ธ ๊ฐ์ง(dao, settingsRepository, todoApiService)๋ฅผ ๋ง๋๋ ์ฝ๋๊ฐ ๋ค๋ฅธ ViewModel์์๋ ํ์ํด์ง๋ฉด, ๋งค๋ฒ ๋๊ฐ์ ์ฝ๋๋ฅผ ๋ณต๋ถํด์ผ ํ๋ค.
Repository๋ก ํ ๋จ๊ณ ๋ ๊ฐ์ธ๊ธฐ
๋จผ์ Room, Retrofit์ ์ง์ ๋ค๋ฃจ๋ ๋ถ๋ถ์ ํ๋์ Repository๋ก ๋ชจ์๋ค.
class TodoRepository @Inject constructor(
private val dao: TodoDao,
private val apiService: TodoApiService
) {
val todoList: Flow<List<Todo>> = dao.getAll()
suspend fun addTodo(title: String) = dao.insert(Todo(title = title))
suspend fun toggleDone(todo: Todo) = dao.update(todo.copy(isDone = !todo.isDone))
suspend fun deleteTodo(todo: Todo) = dao.delete(todo)
suspend fun importFromServer() {
apiService.getTodos().take(5).forEach { remote ->
dao.insert(Todo(title = remote.title, isDone = remote.completed))
}
}
}ViewModel์ ์ด์ "ํ๋ฉด์ ๋ญ ๋ณด์ฌ์ค์ง"๋ง ๊ณ ๋ฏผํ๊ณ , "๋ฐ์ดํฐ๋ฅผ ์ด๋์ ์ด๋ป๊ฒ ๊ฐ์ ธ์ฌ์ง"๋ Repository๊ฐ ์ฑ
์์ง๋ค.
Hilt ์์กด์ฑ ์ถ๊ฐ
// ์ต์์ build.gradle.kts
plugins {
id("com.google.dagger.hilt.android") version "2.60.1" apply false
}
// app/build.gradle.kts
plugins {
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.60.1")
ksp("com.google.dagger:hilt-android-compiler:2.60.1")
implementation("androidx.hilt:hilt-navigation-compose:1.4.0")
}Application ํด๋์ค ํ๋ ๋ง๋ค๊ธฐ
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class TodoApplication : Application()@HiltAndroidApp์ ๋ถ์ธ Application ํด๋์ค๊ฐ ์์ด์ผ Hilt๊ฐ ์ฑ ์ ์ฒด์ ์์กด์ฑ ๊ทธ๋ํ๋ฅผ ๋ง๋ค๊ธฐ ์์ํ๋ค. AndroidManifest.xml์ <application> ํ๊ทธ์ android:name=".TodoApplication"์ ์ถ๊ฐํด์ผ ํ๋ค.
Module - "์ด๊ฑธ ์ด๋ป๊ฒ ๋ง๋๋์ง" ์๋ ค์ฃผ๋ ์ค๋ช ์
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideTodoDatabase(@ApplicationContext context: Context): TodoDatabase =
Room.databaseBuilder(context, TodoDatabase::class.java, "todo.db").build()
@Provides
fun provideTodoDao(database: TodoDatabase): TodoDao = database.todoDao()
@Provides
@Singleton
fun provideTodoApiService(): TodoApiService =
Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TodoApiService::class.java)
}TodoDatabase๋ TodoApiService๋ ์์ฑ์์ @Inject๋ฅผ ๋ถ์ผ ์ ์๋, ์ฐ๋ฆฌ๊ฐ ๋ง๋ ๊ฒ ์๋ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์
์ด๋ค. ๊ทธ๋์ "์ด๊ฑธ ์ด๋ป๊ฒ ๋ง๋๋์ง"๋ฅผ @Provides ํจ์๋ก ์ง์ ์๋ ค์ค์ผ ํ๋ค. @Singleton์ ๋ถ์ธ ๊ฑด ์ฑ ์ ์ฒด์์ ์ธ์คํด์ค๋ฅผ ํ๋๋ง ์ฐ๊ฒ ๋ค๋ ๋ป์ด๋ค. TodoRepository๋ ์๊น ์์ฑ์์ @Inject constructor๋ฅผ ๋ถ์ฌ๋๊ธฐ ๋๋ฌธ์, Hilt๊ฐ TodoDao์ TodoApiService๋ฅผ ์์์ ์ฑ์์ ๋ง๋ค์ด์ค๋ค. ๋ฐ๋ก @Provides๋ฅผ ์ ์จ๋ ๋๋ค.
ViewModel - @HiltViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class TodoViewModel @Inject constructor(
private val repository: TodoRepository
) : ViewModel() {
val uiState: StateFlow<TodoUiState> = repository.todoList
.map { TodoUiState(todoList = it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), TodoUiState(isLoading = true))
fun addTodo(title: String) {
if (title.isBlank()) return
viewModelScope.launch { repository.addTodo(title) }
}
fun toggleDone(todo: Todo) {
viewModelScope.launch { repository.toggleDone(todo) }
}
fun deleteTodo(todo: Todo) {
viewModelScope.launch { repository.deleteTodo(todo) }
}
fun importFromServer() {
viewModelScope.launch {
try {
repository.importFromServer()
} catch (e: Exception) {
// ์๋ฌ ์ฒ๋ฆฌ๋ ์ง๋ ๊ธ๊ณผ ๋์ผ
}
}
}
}AndroidViewModel์์ ํ๋ฒํ ViewModel๋ก ๋์์๋ค. Context๊ฐ ๋ ์ด์ ํ์ ์๋ค. TodoRepository๋ฅผ ์์ฑ์๋ก ๋ฐ๊ธฐ๋ง ํ๋ฉด, ๊ทธ ์์ ๋ TodoDao, TodoApiService๊น์ง Hilt๊ฐ ์ ๋ถ ์ฐ์์ ์ผ๋ก ๋ง๋ค์ด์ ์ฑ์์ค๋ค.
์ปดํฌ์ ๋ธ์์๋ hiltViewModel() ์ฌ์ฉ
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
fun TodoApp(viewModel: TodoViewModel = hiltViewModel()) {
// ์ด์ ๊ณผ ๋์ผ
}viewModel()์ด hiltViewModel()๋ก ๋ฐ๋์๋ค๋ ๊ฒ๋ง ๋ค๋ฅด๋ค. Hilt๊ฐ ๊ด๋ฆฌํ๋ TodoViewModel์ ๊ฐ์ ธ์จ๋ค.
[์ด๋ฏธ์ง: ๋๊ฐ์ด ๋์ํ๋ to-do ์ฑ โ Hilt ์ ์ฉ ์ ํ๋ก ํ๋ฉด ๋์์ ๋ฐ๋ ๊ฒ ์๋ ๋ชจ์ต]
์ ๋ฆฌ
Room,Retrofit์ฒ๋ผ ์์ฑ์์@Inject๋ฅผ ๋ชป ๋ถ์ด๋ ์ธ๋ถ ํ์ ์@Module+@Provides๋ก "๋ง๋๋ ๋ฒ"์ ์๋ ค์ค๋ค- ์์ฑ์์
@Inject constructor๊ฐ ๋ถ์ ๋ด ํด๋์ค(TodoRepository,TodoViewModel)๋ Hilt๊ฐ ํ์ํ ๊ฑธ ์๋์ผ๋ก ์ฑ์์ ๋ง๋ค์ด์ค๋ค ViewModel์@HiltViewModel, ์ปดํฌ์ ๋ธ์์๋viewModel()๋์hiltViewModel()์ ์ด๋ค
์ฌ๊ธฐ๊น์ง ๋ฐ์ดํฐ ์ ์ฅ(Room, DataStore)๊ณผ ์๋ฒ ํต์ (Retrofit), ๊ทธ๋ฆฌ๊ณ ๊ทธ๊ฑธ ๊น๋ํ๊ฒ ์ฎ๋ ๊ตฌ์กฐ(Repository, Hilt)๊น์ง ๋ดค๋ค. ๋ค์ ๊ธ๋ถํฐ๋ ์ปดํฌ์ฆ ์ชฝ์ผ๋ก ๋ค์ ๋์์์, LaunchedEffect ๊ฐ์ ๋ถ์ ํจ๊ณผ๋ฅผ ๋ค๋ฃจ๊ฒ ๋ค.
(์ด ๊ธ์ Hilt ๊ด๋ จ ๋ฒ์ (2.60.1, hilt-navigation-compose:1.4.0)์ ์์ฑ ์์ ๊ธฐ์ค์ผ๋ก ํ์ธํ ๊ฐ์ด๋ค. Hilt์ KSP ์ง์์ ๋น๊ต์ ์ต๊ทผ์ ์์ ํ๋ ์์ญ์ด๋ผ, ์ค์ ๋ก ์ ์ฉํ ๋๋ Hilt ๊ณต์ Gradle ์ค์ ๋ฌธ์์์ ํ์ฌ Kotlin/KSP ๋ฒ์ ๊ณผ ๋ง๋ ์กฐํฉ์ธ์ง ํ ๋ฒ ๋ ํ์ธํ๋ ๊ฒ ์ข๋ค.)
