Development record of developer who study hard everyday.

레이블이 안드로이드힐트인 게시물을 표시합니다. 모든 게시물 표시
레이블이 안드로이드힐트인 게시물을 표시합니다. 모든 게시물 표시
, , , , , , ,

안드로이드 힐트 사용법 및 예제 (코드랩 번역)

 안드로이드 힐트 사용법 및 예제

안드로이드 블로그

MVVM 패턴을 사용할 때 많이 사용하는 힐트를 공부 중이다.

올해 1월에 회사에들어와서 8월달에 첫 신규프로젝트를 진행하면서 이미 사용해봤지만 여전히 낯설다.

코드랩을 같이 따라하면서 힐트 문서의 내용을 복기하고 철저히 이해하도록 해보자.


1. 샘플 코드 클론

$ git clone https://github.com/googlecodelabs/android-hilt


2. 왜 힐트 인가?

class LogApplication : Application() {

lateinit var serviceLocator: ServiceLocator

override fun onCreate() {
super.onCreate()
serviceLocator = ServiceLocator(applicationContext)
}
}

샘플 코드의 LogApplication 클래스에 가보면 ServiceLocator가 있다.

쉽게 생각하면, ServiceLocator가 객체를 만들고 보관하는 컨테이너라고 생각하면된다.

이 컨테이너는 앱의 수명주기와 함께한다.

컨테이너는 필요한 객체를 제공하고 어떻게 만드는지 아는 클래스이다.


규모가 작은 앱에서는 ServiceLocator를 사용해도 되지만 규모가 커질수록 hilt의 사용이 필수적이다.

hilt는 불필요할 보일러플레이트 코드를 제거한다.


3. build.gradle 설정

build.gradle 프로젝트 수준 파일에 가서 아래와 같이 설정해준다.

buildscript {
ext.kotlin_version = '1.7.20'
ext.hilt_version = '2.40.1' //요놈 추가

repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" //요놈 추가
}
}


build.gradle 앱 수준 파일에 가서 아래와 같이 설정해준다. 

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'    //요놈 추가
}
dependencies {

// 요놈들 추가
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}


4. Application class에 힐트 선언하기

샘플코드에서 ServiceLocator를 LogApplication 크래스에서 초기화하고 사용했다.

마찬가지로 힐트에서도 앱의 생명주기와 같이하는 컨테이너를 Application 클래스에 등록해주어야한다.

힐트에서는 간단하게 @HiltAndroidApp 어노테이션을 달아주면 된다.

@HiltAndroidApp
class LogApplication : Application() {

override fun onCreate() {
super.onCreate()
}
}


5. 객체 삽입

힐트를 사용해서 필요한 객체를 주입하려한다.

LogsFragment.kt를 보면 onAttach()에서 객체를 만든다.

@AndroidEntryPoint
class LogsFragment : Fragment() {

@Inject
lateinit var logger: LoggerLocalDataSource
@Inject
lateinit var dateFormatter: DateFormatter

}


✋ 힐트는 현재 Application, Activity, Fragment, View, Service, BroadcastReceiver를 지원한다.

Activity는 FragmentActivity를 상속해야되고 Fragment는 androidx만 지원한다.


하지만, 아직 hilt는 LoggerLocalDataSource와 DateFormatter을 어떻게 만들어야하는지 모른다.

힐트에게 LoggerLocalDataSource와 DateFormatter을 어떻게 만들어야하는지 알려주자.

방법은 간단하다. constructor 앞에 @Inject 어노테이션을 붙여주면 된다.

class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {

}
class DateFormatter @Inject constructor() {

@SuppressLint("SimpleDateFormat")
private val formatter = SimpleDateFormat("d MMM yyyy HH:mm:ss")

fun formatDate(timestamp: Long): String {
return formatter.format(Date(timestamp))
}
}

✋ 힐트가 객체를 어떻게 만드는지 알려주는 것을 binding이라고 한다.

지금까지 우리는 hilt가 2개의 바인딩을 가지고 있는 것을 보았다.

(DateFormatter & LoggerLocalDataSource)


class ServiceLocator(applicationContext: Context) {

private val logsDatabase = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"logging.db"
).build()

val loggerLocalDataSource = LoggerLocalDataSource(logsDatabase.logDao())

fun provideDateFormatter() = DateFormatter()

fun provideNavigator(activity: FragmentActivity): AppNavigator {
return AppNavigatorImpl(activity)
}
}

위의 ServiceLocator 클래스를 보면, LoggerLocalDataSource는 언제나 같은 객체를 제공한다.

이것을 "scoping an instance to a container"라고 한다. (컨테이너에 범위 지정? 이라 해석하면 될까나?)

힐트에서는 이것을 어떻게 할까?


6. 컨테이너에 객체 범위지정

객체의 범위를 컨테이너에 지정할 때는 어노테이션을 사용한다.

각기 다른 생명주기를 갖는 컨테이너들이 있기 때문에 어노테이션의 종류도 다양하다.

여기서는 LoggerLocalDataSource의 범위를 앱 전체로 적용하여 어디서든 같은 LoggerLocalDataSource 객체를 제공하도록 해보자.

@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {

}

한 컨테이너에서 binding이 가능하다면, component hierarchy 상에서 아래에 있는 컨테이너에서도 역시 binding이 가능하다.


다 끝난거 같지만, 아직 문제가 남아있다.

LoggerLocalDataSource를 제공하려면 LogDao가 필요하다.

LoggerLocalDataSource는 LogDao에 종속되어있기 때문이다.

이럴 때는, 힐트에게 LogDao를 어떻게 제공해야하는지 알려주어야한다.

하지만, LogDao는 interface라서 constructor를 가지고 있지 않다,

그럼 어떻게 해야할까?


7. Hilt 모듈

모듈은 힐트에게 binding을 제공하기위해 사용된다.

다시 말해, 생성자 주입이 불가능한 객체들을 binding 하는 방법을 알려준다.

예를 들어, 인터페이스나 앱에 포함되지않은 클래스(외부 라이브러리)가 있다.

힐트모듈은 @Module과 @InstallIn으로 annotate 되어있다.

@Module은 힐트에게 이 부분이 모듈이라고 이야기해주고 @InstallIn은 힐트에게 binding이 어떤 scope에서 이용가능한지 알려준다.


LoggerLocalDataSource 객체를 만들려면 LogDao가 필요하다.

DatabaseModule을 만들어서 LogDao의 binding을 hilt에게 알려주자.

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

}

☝ LoggerLocalDataSource가 application 컨테이너에 범위가 지정되어있다. 

따라서 DatabaseModule도 SingletonComponent 클래스에 설치했다.

 

✋ 코틀린에서, @Provides 함수만 포함하는 모듈이 object 클래스가 될 수 있다.

이러한 방식으로, providers는 최적화 되고 생성된 코드에 inline 될 수 있다.


LogDao 객체의 binding을 제공하려니 또 문제가 있다.

class ServiceLocator(applicationContext: Context) {

private val logsDatabase = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"logging.db"
).build()

val loggerLocalDataSource = LoggerLocalDataSource(logsDatabase.logDao())

fun provideDateFormatter() = DateFormatter()

fun provideNavigator(activity: FragmentActivity): AppNavigator {
return AppNavigatorImpl(activity)
}
}
LogDao 객체를 만들기 위해서는 AppDatabase 객체가 필요하다.

다시 말해, LogDao 객체는 AppDatabase에 종속되어있다.

@Provides는 외부 라이브러리로 생성되는 객체를 주입할 때 사용한다.

@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase { //2
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}

@Provides
@Singleton
fun provideLogDao(database: AppDatabase) : LogDao { //1
return database.logDao()
}
}
1=> LogDao 객체를 만들어주기 위해 provideLogDao 함수를 작성한다.

2 => LogDao 객체를 만들기위해서는 AppDatabase 객체가 필요하다. 따라서 AppDatabase의 binding을 제공하는 provideDatabase 함수를 만든다.

LogDao 객체의 범위가 @Singleton이기 때문에 provideDatabase와 provideLogDao 둘 다 범위를 @Singleton으로 해준다.


마지막으로 LogsFragment가 속한 MainActivity에도 @AndroidEntryPoint를 적어준다.

이건 그냥 약속이다. 왜? 라고 질문해도 의미가 없다.


8. @Binds로 Interface 제공

현재, MainActivity는 ServiceLocator로부터 AppNavigator 객체를 얻고있다.

AppNavigator는 Interface라서 생성자 주입을 사용할 수 없다.

이럴 때, 힐트에게 Interface를 제공하는 방법을 알려주기위해 힐트 모듈에서 @Binds 어노테이션을 사용한다.

@Binds는 반드시 abstract function에 사용해야한다.

abstract function의 리턴 타입은 우리가 제공할 Interface이다.

abstract funcion을 구현할 때 Interface를 제공할 때 필요한 유니크한 매개변수가 필요하다.(ex. AppNavigatorImpl)


이제, AppNavigator 객체를 제공할 함수를 만들어야한다.

그럼, 이전에 만들어둔 DatabaseModule에다가 만들면 안될까?

AppNavigator 객체를 제공할 Module을 따로 만드는게 좋다.

이유는 다음과 같다.

    - 더 나은 구조를 위해서, 모듈의 이름은 모듈이 제공하는 정보를 따라야한다.

    - DatabaseModule은 SingletonComponent의 영역을 갖는다. 하지만 AppNavigator는          Activity 영역만 가진다.

    - 모듈은 @Binds 와 @Provides 어노테이션을 같이 포함할 수 없다.


NavigationModule 클래스를 만들자.

@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}

AppNavigator 객체를 제공하는 abstract function을 모듈 안에 만들어 주었다.

이 함수는 매개변수로 AppNavigatorImpl 객체가 필요하다.

따라서, 우리는 힐트에게 AppNavigatorImpl 객체를 어떻게 만들어줘야하는지 알려주어야한다.

class AppNavigatorImpl @Inject constructor(private val activity: FragmentActivity) : AppNavigator {

override fun navigateTo(screen: Screens) {
val fragment = when (screen) {
Screens.BUTTONS -> ButtonsFragment()
Screens.LOGS -> LogsFragment()
}

activity.supportFragmentManager.beginTransaction()
.replace(R.id.main_container, fragment)
.addToBackStack(fragment::class.java.canonicalName)
.commit()
}
}

AppNavigatorImpl 객체를 만들기 위해서는 FragmentActivity가 필요하다.

그런데 AppNavigator 객체는 Activity Scope에서 제공되기 때문에 FragmentActivity 객체는 힐트가 알아서 제공한다.


이제, MainActivity에서 힐트를 사용해서 AppNavigator 객체를 주입해보자.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var navigator: AppNavigator

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

navigator = (applicationContext as LogApplication).serviceLocator.provideNavigator(this)

if (savedInstanceState == null) {
navigator.navigateTo(Screens.BUTTONS)
}
}

override fun onBackPressed() {
super.onBackPressed()

if (supportFragmentManager.backStackEntryCount == 0) {
finish()
}
}
}


이제, ButtonFragment에서도 힐트를 사용하여 객체를 주입해보자

@AndroidEntryPoint
class ButtonsFragment : Fragment() {

@Inject
lateinit var logger: LoggerLocalDataSource
@Inject
lateinit var navigator: AppNavigator

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_buttons, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.button1).setOnClickListener {
logger.addLog("Interaction with 'Button 1'")
}

view.findViewById<Button>(R.id.button2).setOnClickListener {
logger.addLog("Interaction with 'Button 2'")
}

view.findViewById<Button>(R.id.button3).setOnClickListener {
logger.addLog("Interaction with 'Button 3'")
}

view.findViewById<Button>(R.id.all_logs).setOnClickListener {
navigator.navigateTo(Screens.LOGS)
}

view.findViewById<Button>(R.id.delete_logs).setOnClickListener {
logger.removeLogs()
}
}
}

LoggerLocalDataSource 객체는 LogsFragment에서 사용한 객체와 같은 객체이다.

왜냐하면, application container에 영역을 잡았기 때문이다.

하지만 AppNavigator는 MainActivity에서 사용한 AppNavigator와 다르다.

왜냐하면 ActivityContainer에서 AppNavigator의 영역을 지정하지 않았기 때문이다.


이제, ServiceLocator 클래스는 필요가 없다.

LogApplication 클래스에 가서 ServiceLocator 객체를 지워버리자.

@HiltAndroidApp
class LogApplication : Application() {

override fun onCreate() {
super.onCreate()
}
}


9. Qualifiers

Qualifers가 어떤 역할을 하는지 알아보기위해서 로그 저장방식을 database에서 in-memory list로 바꿔보자.

LoggerDataSource interface를 만든다.

interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}


ButtonsFragment와 LogsFragment에서 주입해주었던 LoggerLocalDataSource를 LoggerDataSource로 바꿔준다.

@AndroidEntryPoint
class ButtonsFragment : Fragment() {

@Inject
lateinit var logger: LoggerDataSource
@Inject
lateinit var navigator: AppNavigator

}
@AndroidEntryPoint
class LogsFragment : Fragment() {

@Inject
lateinit var logger: LoggerDataSource
@Inject
lateinit var dateFormatter: DateFormatter

}


LoggerLocalDataSource가 LoggerDataSource를 implement하게 한다.

@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao): LoggerDataSource {

private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
private val mainThreadHandler by lazy {
Handler(Looper.getMainLooper())
}

override fun addLog(msg: String) {
executorService.execute {
logDao.insertAll(
Log(
msg,
System.currentTimeMillis()
)
)
}
}

override fun getAllLogs(callback: (List<Log>) -> Unit) {
executorService.execute {
val logs = logDao.getAll()
mainThreadHandler.post { callback(logs) }
}
}

override fun removeLogs() {
executorService.execute {
logDao.nukeTable()
}
}
}


LoggerInMemoryDataSource 클래스를 만들어서 LoggerDataSource를 implement하게 한다.

class LoggerInMemoryDataSource : LoggerDataSource {
private val logs = LinkedList<Log>()

override fun addLog(msg: String) {
logs.addFirst(Log(msg, System.currentTimeMillis()))
}

override fun getAllLogs(callback: (List<Log>) -> Unit) {
callback(logs)
}

override fun removeLogs() {
logs.clear()
}
}


LoogerInMemoryDataSource 객체를 사용하기위해서는 hilt가 객체 생성 방법을 알아야한다.

따라서 @Inject를 생성자 앞에 넣어준다.

또한, 액티비티 스코프에서 사용할 것을 @ActivityScoped 어노테이션으로 알려준다.

@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor() : LoggerDataSource {
private val logs = LinkedList<Log>()

override fun addLog(msg: String) {
logs.addFirst(Log(msg, System.currentTimeMillis()))
}

override fun getAllLogs(callback: (List<Log>) -> Unit) {
callback(logs)
}

override fun removeLogs() {
logs.clear()
}
}


hilt는 LoggerInMemoryDataSource와 LoggerLocalDataSource를 제공하는 방법을 안다.

하지만 hilt는 LoggerDataSource를 제공하는 방법을 모른다.


물론, @Binds를 사용해서 LoggerDataSource를 제공하는 방법을 알려주면 된다.

하지만, LoggerInMemoryDataSource와 LoggerLocalDataSource 둘다 필요할 땐 어떻게 해야할까?

LoggingModule.kt를 만들어주고 아래와 같이 2개의 모듈을 만들어준다.

@InstallIn(SingletonComponent::class)
@Module
abstract class LoggingDatabaseModule {
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource) : LoggerDataSource
}

@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource) : LoggerDataSource
}

힐트에게는 LoggerDataSource 객체를 제공하는 2개의 모듈을 구분할 수 있는 무언가가 필요하다.

그것이 바로 @Qualifier이다. 

즉, @Qualifier는 binding을 구분하는 어노테이션이다.


LoggingModule.kt에 @Qualifier를 정의해준다.

@Qualifier
annotation class InMemoryLogger

@Qualifier
annotation class DatabaseLogger

@InstallIn(SingletonComponent::class)
@Module
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource) : LoggerDataSource
}

@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource) : LoggerDataSource
}


그리고 아래와 같이 객체를 주입하는 시점에도 @Qualifer 어노테이션을 사용해야한다.

@AndroidEntryPoint
class ButtonsFragment : Fragment() {

@InMemoryLogger
@Inject
lateinit var logger: LoggerDataSource
@Inject
lateinit var navigator: AppNavigator
}
@AndroidEntryPoint
class LogsFragment : Fragment() {

@Inject
lateinit var logger: LoggerDataSource
@Inject
lateinit var dateFormatter: DateFormatter
}


10. UI 테스트

일단, 패스


11. @EntryPoint 어노테이션

@EntryPoint는 Hilt가 지원하지않는 클래스나 Hilt를 사용할 수 없는 클래스에서 객체를 주입할 때 사용하는 어노테이션이다.

@EntryPoint는 Hilt가 관리하는 컨테이너의 진입점이다.


어플리케이션 바깥으로 로그를 보내고싶다고 하자.

이럴 때, ContentProvider가 필요하다.

바깥에서 ContentProvider를 사용해서 로그를 query만 할 수 있게 허용하자

지금의 샘플코드는 Room 데이터베이스를 이용해서 로그를 가져온다. 따라서, LogDao 클래스가 Cursor를 사용하여 필요한 데이터를 제공하는 메소드를 선언해야한다.

LogDao.kt 파일에 아래처럼 코드를 추가해주자

@Dao
interface LogDao {

@Query("SELECT * FROM logs ORDER BY id DESC")
fun getAll(): List<Log>

@Insert
fun insertAll(vararg logs: Log)

@Query("DELETE FROM logs")
fun nukeTable()

//여기서부터
@Query("SELECT * FROM logs ORDER BY id DESC")
fun selectAllLogsCursor(): Cursor

@Query("SELECT * FROM logs WHERE id = :id")
fun selectLogById(id: Long) : Cursor?
//여기까지 추가
}


이제, Cursor를 리턴하는 query 메소드를 override하는 ContentProvider를 만들어준다.

이름은 LogsContentProvider 클래스라고 하자.


private const val LOGS_TABLE = "logs"
private const val AUTHORITY = "com.example.android.hilt.provider"
private const val CODE_LOGS_DIR = 1
private const val CODE_LOGS_ITEM = 2

/**
* A ContentProvider that exposes the logs outside the application process.
*/
class LogsContentProvider : ContentProvider() {

private val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, LOGS_TABLE, CODE_LOGS_DIR)
addURI(AUTHORITY, "$LOGS_TABLE/*", CODE_LOGS_ITEM)
}

override fun onCreate(): Boolean {
return true
}

override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?,
): Cursor? {
val code : Int = matcher.match(uri)
return if(code == CODE_LOGS_DIR || code == CODE_LOGS_ITEM) {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val logDao: LogDao = getLogDao(appContext)

val cursor: Cursor? = if(code == CODE_LOGS_DIR) {
logDao.selectAllLogsCursor()
} else {
logDao.selectLogById(ContentUris.parseId(uri))
}
cursor?.setNotificationUri(appContext.contentResolver, uri)
cursor

} else {
throw IllegalArgumentException("Unknown URI: $uri")
}
}

override fun getType(p0: Uri): String? {
throw UnsupportedOperationException("Only reading operations are allowed")
}

override fun insert(p0: Uri, p1: ContentValues?): Uri? {
throw UnsupportedOperationException("Only reading operations are allowed")
}

override fun delete(p0: Uri, p1: String?, p2: Array<out String>?): Int {
throw UnsupportedOperationException("Only reading operations are allowed")
}

override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
throw UnsupportedOperationException("Only reading operations are allowed")
}

}

아직 getLogDao(appContext)를 구현하지 않았다.

LogDao 객체를 주입해야하는데 힐트는 ContentProvider 컨테이너를 지원하지 않는다.

따라서 우리가 @EntryPoint를 통해서 만들어주어야한다.


LogsContentProvider 클래스 안에 LogsContentProviderEntryPoint를 만들어준다.

class LogsContentProvider : ContentProvider() {
    //1
@InstallIn(SingletonComponent::class)
@EntryPoint
interface LogsContentProviderEntryPoint {
fun logDao(): LogDao
}
    //2
private fun getLogDao(appContext: Context): LogDao {
val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, LogsContentProviderEntryPoint::class.java)
return hiltEntryPoint.logDao()
}
}

1 => @EntryPoint 어노테이션을 달아준 interface를 선언하고 SingletonComponent에 @InstallIn 해준다.

왜냐하면 LogDao가 앱 전체에 동일한 객체로 쓰이길 원하기 때문이다.

Interface안에 LogDao의 binding을 제공하는 함수를 선언한다.

2 => entry point에 진입하기위해서는 EntryPointAccessors의 static 메소드를 사용한다.

이때, 매개변수로 컴포넌트 객체를 넘겨주는데 이 컴포넌트 객체 매개변수, static 메소드, InstallIn 하는 범위가 모두 같아야한다!

예를 들어, 위의 예에서 appContext, @InstallIn(SingletonComponent::class), EntryPointAccessors.fromApplication이 앱 컨테이너를 가리킨다.

이제 hilt가 지원하지않는 contentProvider에서도 힐트를 사용하여 필요한 객체를 주입할 수 있다.

 


Share:
Read More
, , , , , , , , ,

안드로이드 힐트(hilt)로 뷰모델 주입하기

안드로이드 힐트(Hilt)로 뷰모델 주입하기

안드로이드 개발

회사에서 신규 프로젝트를 하면서 MVVM 패턴을 사용하고 있다.

MVVM 패턴을 사용할 때 거의 필수인 Hilt를 사용하여 프로젝트를 진행했다.

오늘은 가장 흔히 사용되는 뷰모델(ViewModel)을 주입하는 방법을 소개한다.


먼저, 힐트를 위한 기본 세팅을 해야하는데 이전 글을 참고하길 바란다.

1. ktx dependency 추가하기

dependencies {

//ktx
implementation "androidx.activity:activity-ktx:1.5.0"
implementation "androidx.fragment:fragment-ktx:1.5.0"
}

앱 수준의 gradle 파일에 activity-ktx, fragment-ktx를 추가해준다.

뷰모델 주입을 간편하게 해준다.


2. @AndroidEntryPoint 주석 달기

@AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>(){}

뷰모델을 주입할 액티비티나 프래그먼트에 @AndroidEntryPoint 주석을 단다.

이 컴포넌트에 힐트를 이용하여 객체를 주입할 것이다 라는 신호이다.


3. Hilt에 뷰모델을 주입하는 방법 알려주기

@HiltViewModel
class MainViewModel @Inject constructor(
application: Application,
val preferenceRepository: PreferenceRepository
) : BaseViewModel(application) {

Hilt가 객체를 주입하려면 그 객체를 어떻게 생성해야하는지 알아야한다.

Hilt는 이것을 생성자 주입을 통해 객체의 생성 방식을 알게된다.

일단, 우리는 뷰모델을 주입할 것이기 때문에 ViewModel 클래스 위에 @HiltViewModel 어노테이션을 달아준다.

그리고 constructor 앞에 @Inject 어노테이션을 적어주면서 힐트에게 이 객체를 만들 때 필요한 객체를 주입한다.

(동시에 힐트에 뷰모델을 만들 때 이러한 객체들이 필요하다는 것을 알려주게 된다.)

가끔, 다른 블로그보면 @ViewModelInject 어노테이션을 쓰는 경우가 보이는데 이것은 이제 deprecated 되었다!

👉참고로 Hilt에서는 기본적으로 제공하는 객체들이 있다.

위의 코드에서는 application이 그 예이다.

따로 어노테이션을 달아주지 않아도 Hilt는 application 객체를 알아서 주입해준다.

이와 같은 객체에는 ActivityContext가 있다.


4. 액티비티에 뷰모델 변수 선언하기


@AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>(){

private val mainViewModel: MainViewModel by viewModels()

}
액티비티에 뷰모델을 선언해주면된다.

이때, activity-ktx에서 제공하는 by viewModels() 를 사용하면 위임을 통해 뷰모델을 쉽게 주입할 수 있다.

주의할 점은 @Inject 어노테이션이 필요없다!

                                                                     
 

이번에는 프래그먼트에서 액티비티 뷰모델을 참조하는 방식을 알아보자.

너무 간단하다. 쫄지말자 ㅋ


@AndroidEntryPoint
class MeasureFragment : BaseFragment<FragmentMeasureBinding>(), OnMapReadyCallback {
override val layoutResID: Int
get() = R.layout.fragment_measure

@Inject
lateinit var preferenceRepository: PreferenceRepository

private val measureViewModel: MeasureViewModel by viewModels()
private val mainViewModel: MainViewModel by activityViewModels() }

우선, Fragment 클래스 위에 @AndroidEntryPoint를 적어준다.

그리고 액티비티에서 주입한 뷰모델을 선언해준다.

이때는 by viewModels()가 아니라 by activityViewModels()를 통해 뷰모델을 선언해주면 된다.

이제 액티비티에서 사용하는(액티비티 컴포넌트에서 선언된) 뷰모델을 공유해서 사용할 수 있다.

이렇게하면 데이터를 액티비티와 프래그먼트가 손쉽게 공유해서 앱을 만들 수 있다.

 


Share:
Read More
, , , , ,

안드로이드 힐트(hilt) 사용하기

안드로이드 Hilt 사용하기

안드로이드 블로그

회사에서 첫 신규 프로젝트를 하면서 MVVM 구조를 적용하게 되었다.

이에 거의 필수적인 요소인 Hilt를 처음 프로젝트에 적용해보았다.

그 동안 코드랩을 보면서 연습만해봤지 프로젝트에 적용하는 것은 처음이랑 긴장도 되었지만 무사히 프로젝트를 마치게 되었다.

다시 한 번 복습도 할 겸 Android Hilt를 적용하는 과정을 기록해보려한다.


1. classpath 추가

buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.4"
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'

classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

프로젝트 수준 gradle 파일에 classpath를 추가해준다.


2. dependency와 plugins id 추가

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
} dependencies {     //hilt     implementation "com.google.dagger:hilt-android:2.38.1"     kapt "com.google.dagger:hilt-compiler:2.38.1" }

앱 수준의 gradle 파일에 위와같이 plugin과 dependency를 추가해준다.


3. application 선언

@HiltAndroidApp
class MainApplication: Application() {

}

Application() 클래스를 상속하는 클래스를 만들어주고 @HiltAndroidApp 어노테이션을 추가해준다.

여기서 주의할 점이 매니페스트 파일에서도 이 클래스를 선언해주어야한다.

아래 코드 참고

<application
android:name="com.appg.joowon.MainApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.JooWon">


</application>


이제 힐트를 사용할 준비가 끝났다.

힐트는 Jetpack Architecture에 최적화 되어있는 Dependency Injection이기 때문에 MVVM을 적용하기에 더 편하고 간결하다.

다음 글에서는 액티비티, 뷰모델 등등 다양한 컴포넌트들에서 hilt를 이용하여 객체를 주입하는 방법을 소개하도록 하겠다.


Share:
Read More