Hanbit the Developer
Android Dependent Use Case 이슈 해결 본문
문제 상황
앱에서 계정을 생성했을 때 그 계정에 종속되는 TutorialTask 리스트를 초기화해야 한다. AddUserUseCase 클래스에서 InitializeTutorialTasksUseCase를 호출하고자 한다:
class AddUserUseCase @Inject constructor(
private val userRepository: UserRepository,
private val appStateRepository: AppStateRepository,
private val initializeTutorialTasksUseCase: InitializeTutorialTasksUseCase,
) {
suspend operator fun invoke(user: User) {
val addedUserId = userRepository.insertUser(user)
initializeTutorialTasksUseCase(addedUserId) // Here!
// ...
}
문제는 InitializeTutorialTasksUseCase를 구현할 때 발생한다. core.domain 모듈은 의존성 그래프 상 최하단에 존재하며 순수 Kotlin 모듈인데, TutorialTask를 생성할 때 R.drawable.rocks 와 같은 안드로이드에 의존하는 코드가 필요하다는 점이 문제이다.
TutorialTask(day = 2, name = "...", detailImageId = R.drawable.eight_rocks, userId = userId, isDone = false),
Attemption
Hilt로 의존성 주입을 하면 해결되지 않을까 하고 List<TutorialTask>를 반환하는 provides 함수를 구현을 해보았는데, 간과한 것이 있었다. TutorialTask가 userId에 의존하기 때문에 상황에 맞는 userId 인자가 필요하다는 것이다. 따라서 다른 방법이 필요하다.
우선 고정할 수 있는 것부터 고정해보자.
안드로이드에 의존하기 때문에 TutorialTask 인스턴스 생성 함수를 상위 모듈에 두어야 한다:
// feature.tutorial module
// TutorialTask.kt
fun TutorialTask.Companion.createInitialTutorialTasks(userId: Int): List<TutorialTask> = listOf(
// ...
TutorialTask(day = 2, name = "...", detailImageId = R.drawable.eight_rocks, userId = userId, isDone = false),
// ...
)
다음 스텝은 어떤 ‘마법’이 일어나서 core.domain에서 상위 모듈에 있는 createInitialTutorialTasks 를 호출할 수 있어야 한다. 아래 코드에서 당연히 createInitialTutorialTasks 를 호출할 수 없다:
class InitializeTutorialTasksUseCase @Inject constructor(
private val tutorialTaskRepository: TutorialTaskRepository,
) {
suspend operator fun invoke(userId: Int) {
// val tutorialTasks = TutorialTask.createInitialTutorialTasks(userId)
tutorialTaskRepository.insertTutorialTasks(tutorialTasks)
}
}
Solution
위 코드가 상위 안드로이드 모듈에서 실행되어야만 한다는 점에서 떠오른 방법이다. 유스케이스의 인터페이스를 core.domain에 두고 상위 모듈에서 이를 구현하고 Hilt로 구현체를 전달하는 방식이다:
core.domain module
/**
* 특정 계정에 대한 [TutorialTask]를 초기화하는 유스케이스입니다. [TutorialTask]가 android resource id에 의존하기
* 때문에 상위 안드로이드 모듈에서 DI로 구현체를 넣어주는 방식을 선택하였습니다.
*/
interface InitializeTutorialTasksUseCase {
suspend operator fun invoke(userId: Int)
}
feature.tutorial module
class InitializeTutorialTasksUseCaseImpl @Inject constructor(
private val tutorialTaskRepository: TutorialTaskRepository,
) : InitializeTutorialTasksUseCase {
override suspend operator fun invoke(userId: Int) {
val tutorialTasks = TutorialTask.createInitialTutorialTasks(userId)
tutorialTaskRepository.insertTutorialTasks(tutorialTasks)
}
}
@Module
@InstallIn(SingletonComponent::class)
interface InitializeTutorialTasksUseCaseModule {
@Binds
fun bindsInitializeTutorialTasksUseCase(
initializeTutorialTasksUseCaseImpl: InitializeTutorialTasksUseCaseImpl,
): InitializeTutorialTasksUseCase
}
마치며
사실 엔티티에 안드로이드 리소스 id가 있는 것 자체가 문제가 된다. 도메인, 데이터 영역을 UI로 물들인 데다가 만약 리소스가 변경되면 일일이 고쳐줘야 할 수도 있다.
서버 개발자 없이 혼자 하는 토이 프로젝트여서 불가피 하긴 했지만(서버가 들어갔다면 resource id 대신 imageUrl이 들어갔을 것임) 개선 방안을 생각하면 좋을 듯 하다. 엔티티에서 안드로이드 종속 필드를 제거하고, 관련 내용을 기능 모듈에서 처리한다든지..?
(추가: 2024-08-23)
앱을 업데이트 하고 이미지가 안 보이거나 다른 리소스를 보여주는 문제가 발생했습니다. 이전 버전에서 유효하던 리소스 ID가, 앱이 다시 빌드되고 업데이트 한 이후에 유효하지 않게 된 것으로 보입니다. 리소스 ID 대신 리소스의 이름을 기반으로 동적으로 리소스를 가져오는 방법이 있습니다. 하지만 제 경우 이미지 리소스가 사용자에게 자주 보여지는 이미지가 아니기 때문에 리모트 URL을 읽는 방식으로 변경하게 되었습니다.(firebase 사용)
'Android' 카테고리의 다른 글
Retrofit2 Implementation(1): Top-Down from Retrofit.create() (1) | 2024.02.26 |
---|---|
[Compose] SwipeToAction 구현(AnchoredDraggableState, Layout) (0) | 2024.02.16 |
UI State Flow 이슈 해결 (0) | 2024.01.25 |
Android Document | Processes and app lifecycle (3) | 2024.01.25 |
Android Document | App startup time (1) | 2024.01.25 |