#1 개요
#1-1 Hilt 도입
계획표 상, Hilt 도입은 끝자락 단계였다. 하지만, 코드를 짜가며 앱을 구현해나가는 데에 여러 상용구 코드들이 나를 거슬리게 했다. Hilt로 현존하는 상용구 코드 그리고 잠재적으로 발생할 상용구 코드들을 선제적으로 제거하는 편이 더 좋을 것이란 판단을 내렸다.
#1-2 기반 (레퍼런스)
위 게시글에 기반해, 본 안드로이드에 Hilt를 도입한다.
#2 코드 - 환경 설정
#2-1 모듈 수준 build.gradle.kts
plugins {
...
// KSP 및 kapt (어노테이션 읽기용)
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.kapt")
// Hilt
id("com.google.dagger.hilt.android")
}
android {
...
}
dependencies {
...
// Hilt
implementation(libs.hilt.android)
kapt(libs.hilt.android.compiler)
ksp(libs.hilt.compiler)
implementation(libs.androidx.hilt.navigation.compose) // Hilt와 Jetpack Compose의 ViewModel을 함께 사용할 수 있게 해주는 라이브러리 (hiltViewModel() 사용 가능)
}
#1-2에 링크한 레퍼런스 게시글의 build.gradle만으로는 후술할 hiltViewModel()을 사용할 수 없다. hiltViewModel()을 사용할 수 있는 라이브러리인 "androidx.hilt:hilt-navigation-compose"를 dependencies에 넣었다.
#2-2 Application 클래스
package com.example.nutri_capture_new.di
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class HiltApplication : Application()
#1-2에 의하면, Hilt는 @HiltAndroidApp 어노테이션이 붙은 Application 클래스를 요구한다. 또, 이 Application 클래스 및 의존성 주입에 관련된 모든 클래스를 넣을 패키지인 "di" 패키지를 신설했다.
#2-3 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".di.HiltApplication"
...>
<activity
...
</activity>
</application>
</manifest>
방금 만든 Application 클래스를 추가한다.
#3 코드 - Activity
#3-1 Activity
...
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// nutrientViewModel이 Hilt의 viewModels()에 의해 관리되도록 위임(by). 따라서 반드시 by 키워드로 선언해야함.
private val nutrientViewModel: NutrientViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
NutricapturenewTheme {
Scaffold(
...
bottomBar = {
NutrientChatBar()
}
) { ... ->
Box(
...
) {
NutrientScreen()
}
}
}
}
}
}
@AndroidEntryPoint가 선언된 Activity에서 ViewModel을 만들면, 이 Activity 내의 모든 하위 Composable에서 동일한 ViewModel (싱글톤) 인스턴스를 공유한다. 하위 컴포저블 함수인 NutrientChatBar()나 NutrientScreen()에서 nutrientViewModel를 공유해서 사용한다는 얘기다.
#3-2 Activity의 하위 컴포저블
@Composable
fun NutrientScreen(
viewModel: NutrientViewModel = hiltViewModel()
) {
...
}
@Composable
fun NutrientChatBar(
viewModel: NutrientViewModel = hiltViewModel()
) {
...
}
hiltViewModel()을 사용하면 현재 Activity나 Fragment에서 Hilt가 관리하는 ViewModel 인스턴스를 자동으로 가져온다 (Hilt가 관리하는 ViewModel이란 @HiltViewModel 어노테이션이 붙은 ViewModel을 의미한다). hiltViewModel()은 ViewModelStoreOwner(예: Activity, Fragment)의 범위 내에서 ViewModel을 자동으로 찾아 주입하는 방식으로 작동한다. 이 코드에서는 #3-1의 nutrientViewModel를 가져오게 된다.
만약 viewModelStoreOwner에 해당 ViewModel이 없다면, 다시 말해 여기선 MainActivity 내에 nutrientViewModel이 없다면 어떻게 될까? 이 경우 hiltViewModel()은 새로운 ViewModel 인스턴스를 (어디에서 가져오는게 아니라 그 자리에서) 생성하고 관리한다. 이 경우에, ViewModel의 생명주기(범위)는 해당 컴포저블의 생명주기에 종속된다.
프로젝트에 있던 ViewModelFactory는 이제 삭제한다. Hilt 라이브러리에서 알아서(암시적으로) ViewModel에 인수를 주입할 것이기 때문이다.
#4 코드 - Room
#4-1 Database, DAO
package com.example.nutri_capture_new.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(
entities = [
Day::class,
Meal::class
],
views = [DayMealView::class],
version = 1
)
@TypeConverters(Converters::class)
abstract class MainDatabase : RoomDatabase() {
abstract val mainDAO: MainDAO
/* 삭제
companion object {
@Volatile
private var INSTANCE: MainDatabase? = null
fun getInstance(context: Context): MainDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
MainDatabase::class.java,
"main_database"
).build()
INSTANCE = instance
}
return instance
}
}
}
/*
}
데이터베이스 인스턴스를 만드는 기존 방식은 MainDabase의 companion object를 이용하는 거였다. 이제는 Hilt를 통해 암시적으로 이 작업을 수행할 것이므로 해당 코드를 제거한다.
package com.example.nutri_capture_new.di
import android.content.Context
import androidx.room.Room
import com.example.nutri_capture_new.db.MainDAO
import com.example.nutri_capture_new.db.MainDatabase
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 DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): MainDatabase {
return Room.databaseBuilder(
context,
MainDatabase::class.java,
"main_database"
).build()
}
@Provides
fun provideMainDAO(database: MainDatabase): MainDAO {
return database.mainDAO
}
}
데이터베이스 및 DAO는 @Inject 대신 @Provides 방식을 사용해야 한다. 공식 문서에 따르면, "클래스가 외부 라이브러리에서 제공되므로 클래스를 소유하지 않은 경우(Retrofit, OkHttpClient 또는 Room 데이터베이스와 같은 클래스) 또는 빌더 패턴으로 인스턴스를 생성해야 하는 경우"에는 생성자 삽입(@Inject 어노테이션)이 불가능하기 때문이다 (참조: @Provides의 목적).
이 코드 이후로, 이제 Provides 방식으로 데이터베이스 및 DAO가 주입된다. provideDatabase()가 데이터베이스 인스턴스를 provideMainDAO()에 전달하고, provideMainDAO()는 DAO 인스턴스를 뱉는다.
#4-2 Repository
package com.example.nutri_capture_new.db
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import javax.inject.Inject
class MainRepository @Inject constructor(private val dao: MainDAO) {
...
}
provideMainDAO()에 의해 인수 dao가 주입된다.
#4-3 ViewModel
...
@HiltViewModel
class NutrientViewModel @Inject constructor(private val repository: MainRepository) : ViewModel() {
...
}
인수 repository의 클래스명 MainRepository를 Hilt가 감지 + 검색하여 그 인스턴스를 알아서 주입해준다.
#5 완성된 앱
#5-1 의존성 그래프
본 게시글에서 다룬 클래스들의 의존성을 도식표로 표현하면 위와 같다. 화살표는 클래스 간의 종속성을 나타낸다. 예를 들어, MainActivity는 NutrientScreen에 종속된다. 종속의 사전적 의미는 '자주성이 없이 주가 되는 것에 딸려 붙음'이다. 종속은 '알아야 한다'라는 말로도 표현할 수 있다. 따라서 MainActivity는 NutrientScreen에 대해 알아야 한다. 반면, NutrientScreen은 MainActivity를 몰라도 된다. NutrientScreen을 설계할 땐 MainActivity에서 뭘 어떻게 할지 전혀 신경쓰지 않아도 된다는 것이다 (대신, NutrientScreen은 NutrientViewModel에 대해 종속적이므로 NutrientViewModel을 참조하며 설계해야 한다). '알아야 하는 쪽'에서 '몰라도 되는 쪽'으로 화살표를 이은 것이 위 도식도다.
#5-2 이 게시글 시점의 Commit
#5-3 본 프로젝트의 가장 최신 Commit
'개발 일지 > Nutri Capture' 카테고리의 다른 글
Nutri Capture 프론트엔드 - windowInsetsPadding() (0) | 2025.01.29 |
---|---|
Nutri Capture - 코드 정리 (0) | 2025.01.29 |
Nutri Capture 프론트엔드 - 아이콘 제작 (1차) (0) | 2025.01.12 |
Nutri Capture 백엔드 - StateFlow로 전환 (0) | 2025.01.02 |
Nutri Capture 백엔드 - ChatBar에 ViewModel 연결 (0) | 2025.01.02 |