class KoinAndroidUnitTestRule(
private val modules: List<Module>,
) : TestWatcher() {
constructor(vararg modules: Module) : this(modules.toList())
override fun starting(description: Description) {
if (getKoinApplicationOrNull() == null) {
startKoin {
androidContext(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext)
modules(modules)
}
} else {
loadKoinModules(modules)
}
}
override fun finished(description: Description) {
stopKoin()
// unloadKoinModules(modules)
}
}
finished 에서 stopKoin 과 unloadKoinModules(modules) 했을 때으 ㅣ차이는??
@RunWith(AndroidJUnit4::class)
@Config(application = TestApplication::class)
class PokemonDetailActivityTest {
@get:Rule
val activityRule = activityScenarioRule<PokemonDetailActivity>()
private val scenario get() = activityRule.scenario
@get:Rule
val koinTestRule =
KoinAndroidUnitTestRule(
testViewModelModule,
)
@Test
fun Activity 실행 테스트()
{
scenario.onActivity { activity ->
activity.shouldNotBeNull()
}
}
@Test
fun Activity 구성 변경 테스트()
{
// given
var previewVm: PokemonDetailViewModel? = null
scenario.onActivity { activity ->
previewVm = activity.getViewModel()
}
// when
scenario.recreate()
scenario.onActivity {
it.getViewModel<PokemonDetailViewModel>() shouldBe previewVm
}
}
}
이 액티비티 테스트에서 이런 식으로 두개의 테스트를 돌리면
하나의 테스트는 통과하고 하나의 테스트는 KoinApplication has not been started 가 되면서 실패하는 이유는?
이 상황에서 발생하는 에러는 KoinApplication has not been started 이다.
이 에러는 Koin 이 어떻게 시작되고 중지되는지에 관련된 문제이다.
특히, 이 문제는 KoinAndroidUnitTestRule 의 finished 메소드에서 stopKoin()의 동작과 관련이 있다.
stopKoin 과 unloadKoinModules()의 주요 차이점
stopKoin()
: 이것은 Koin context를 완전히 종료하며 Koin application 자체도 포함된다. 이 메소드가 호출되면 전체 의존성 주입 설정 (모듈, 정의 등)이 파괴되고 Koin
context가 더 이상 사용할 수 없다. 이것은stopKoin()
이후에 Koin이 명시적으로 다시startKoin()
으로 재시작될 때까지 효과적으로 "꺼진" 상태임을 의미한다.unloadKoinModules(modules)
: 이것은 전체 Koin context를 중지하지 않고 특정 모듈을 언로드한다. 이것은 특정 모듈의 정의를 Koin application에서 제거하지만
Koin 환경을 유지한 채로 둔다.
문제점
너의 코드에서는 finished() 메소드에서 stopKoin()을 사용하고 있는데, 이것은 각 테스트 후에 Koin을 완전히 중지한다. 그러나 starting() 메소드에서는 Koin이 이전에 시작되지 않았을 때만
다시 시작된다. 이 로직은 여러 테스트를 실행할 때 문제를 일으킨다.
- 첫 번째 테스트:
- Koin은 starting()에서 시작된다.
- 테스트는 성공적으로 실행된다.
- 테스트가 끝나면
stopKoin()
이 호출되어 Koin을 완전히 종료한다.
- 두 번째 테스트:
- 첫 번째 테스트 이후에
stopKoin()
이 호출되었기 때문에 Koin은 완전히 중지되었다. - starting() 메소드는
getKoinApplicationOrNull()
을 확인하지만 Koin이 중지되었기 때문에null
을 반환한다. 따라서 Koin을 다시 시작한다. - 테스트가 시작되기 전에 Koin이 완전히 사용 가능하지 않을 때 비동기 작업이나 타이밍 문제가 있는 경우 문제가 발생한다.
- 이로 인해 두 번째 테스트에서
KoinApplication has not been started
오류가 발생한다.
- 첫 번째 테스트 이후에
하나는 통과하는데 다른 테스트들은 터지는 이유
stopKoin() 을 사용하고 있는데, 두번째 테스트에서 KoinApplication has not been started 가 발생하는 이유는 두번째 테스트가 실행되기 전에 Koin이 완전히 시작되지 않았기
때문이다.
Solution
1 stopKoin()
대신 unloadKoinModules(modules)
사용
완전히 Koin을 중지하는 대신 각 테스트 후에 테스트 모듈을 언로드할 수 있다. 이렇게 하면 Koin context가 테스트 간에 유지되지만 특정 모듈의 정의가 언로드되어 다음 테스트에 영향을 미치지 않는다.
override fun finished(description: Description) {
unloadKoinModules(modules)
}
이 접근은 Koin이 테스트 간에 초기화된 상태를 유지하면서 이전 테스트에서 사용한 모듈을 지우므로 다음 테스트에서 적절한 설정이 가능하다.
2. 각 테스트에서 명시적으로 Koin 시작 및 중지
만약 각 테스트마다 Koin을 완전히 재시작하길 원한다면, KoinAndroidUnitTestRule
을 조정하여 Koin이 매번 재시작되도록 할 수 있다. 이렇게 하면 여전히 stopKoin()
을 사용할 수
있지만, 각 테스트 시작 시 Koin이 항상 새로 시작되도록 보장해야 한다.
override fun starting(description: Description) {
stopKoin() // Ensure Koin is stopped before starting
startKoin {
androidContext(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext)
modules(modules)
}
}
이렇게 하면 각 테스트마다 Koin이 완전히 재시작되므로 잔류 상태나 부분적 초기화 문제를 방지할 수 있다.
Recommended Option
추천되는 옵션
I recommend using option 1, where you switch from stopKoin()
to unloadKoinModules(modules)
. This is a cleaner
solution because it avoids the overhead of starting and stopping Koin for every test, and it ensures that the Koin
context stays alive across the test suite. By only unloading the specific test modules, you avoid the potential issues
that arise from completely shutting down Koin.
stopKoin() 대신 unloadKoinModules(modules) 사용하는 option 1 추천.
요약
finished()
에서stopKoin()
대신unloadKoinModules(modules)
사용- 이 접근은 테스트 간에 Koin을 유지하면서 테스트별 모듈을 지우므로 테스트 간 영향을 줄인다.
- 대신에, 각 테스트마다 Koin을 완전히 재시작하길 원한다면, 각 테스트 시작 시
stopKoin()
을 항상 호출하도록 보장해야 한다. 그러나 이 방법은 약간 테스트가 느려질 수 있다.
로그 찍어보기
class KoinAndroidUnitTestRule(
private val modules: List<Module>,
) : TestWatcher() {
constructor(vararg modules: Module) : this(modules.toList())
override fun starting(description: Description) {
println("starting called")
if (getKoinApplicationOrNull() == null) {
println("startKoin called")
startKoin {
androidContext(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext)
modules(modules)
}
} else {
println("startKoin is not called just load koin modules")
loadKoinModules(modules)
}
}
override fun finished(description: Description) {
println("finished called")
stopKoin()
println("stopKoin called")
println("getKoinApplicationOrNull(): ${getKoinApplicationOrNull()}")
}
}
이미지 | 이미지 |
---|---|
이상하다 finished 호출, stopKoin 후에도 getKoinApplicationOrNull(): org.koin.core.KoinApplication@5a791f63
이 찍힘다.
코인에서는, Koin
과 KoinApplication
은 관련이 있지만 서로 다른 목적을 가지고 있다.
이 두 개념 사이의 차이를 이해하면 코인이 어떻게 응용 프로그램 전반에 걸쳐 의존성 주입을 관리하는지 명확해진다.
1. KoinApplication
- 정의:
KoinApplication
은 Koin 의존성 주입 (DI) 시스템의 전체 구성을 나타낸다.
DI 시스템의 컨텍스트를 설정하고 관리하는 책임이 있다. 모듈을 로드하고 로깅을 구성하며 필요한 다른 구성 요소를 초기화한다. - 역할:
KoinApplication
은 Koin 환경의 초기화 및 관리를 담당한다. Koin을 설정하고 모듈을 로드하며 DI 컨테이너를 설정하는
애플리케이션의 구성을 유지한다. - 생명 주기:
startKoin()
가 호출될 때KoinApplication
객체가 생성된다. 이 객체는 수명 동안 전역 Koin 컨텍스트를
관리하고 모듈의 로드/언로드, Koin의 시작 및 기타 구성을 조정한다. - 기능:
- Koin을 초기화한다.
- 전체 의존성 주입 설정 (모듈, 로깅 등)을 보유한다.
- 초기화 중에 인스턴스를 즉시 생성할 수 있다 (createEagerInstances()를 통해).
Koin
인스턴스 및 기타 구성 상태를 위한 컨테이너이다.
요약: KoinApplication
은 Koin의 전체 설정 컨테이너로, Koin 시스템의 전반적인 설정 및 수명 주기를 관리한다.
2. Koin
- Definition:
Koin
is the actual DI container that holds all the services and definitions you've registered in
your modules. It's the object that is used to resolve dependencies during runtime. - Role:
Koin
manages the creation, retrieval, and lifecycle of objects defined in the modules. It's the "core" of
the DI system that provides instances of the dependencies when requested. This is where all your services (singletons,
factories, etc.) live and are resolved. - Lifecycle: The
Koin
object is instantiated when theKoinApplication
is started. TheKoin
instance is held
byKoinApplication
, and it's what actually processes DI requests and provides instances. - Functionality:
- It handles requests to inject dependencies.
- It maintains the lifecycle of instances (e.g., singleton, factory).
- It manages module loading and unloading at runtime.
- It provides APIs to retrieve objects, such as
get<T>()
orinject<T>()
.
- 정의:
Koin
은 실제 DI 컨테이너로, 모듈에 등록된 모든 서비스와 정의를 보유한다. 런타임 중에 의존성을 해결하는 데 사용되는 객체이다. - 역할:
Koin
은 모듈에서 정의된 객체의 생성, 검색 및 수명 주기를 관리한다. 요청 시 의존성의 인스턴스를 제공하는 DI 시스템의 "핵심"이다. 이곳에 모든 서비스 (싱글톤, 팩토리 등)가 존재하고
해결된다. - 생명 주기:
Koin
객체는KoinApplication
이 시작될 때 인스턴스화된다.Koin
인스턴스는KoinApplication
에 의해 보유되며, DI 요청을 처리하고 인스턴스를
제공하는 역할을 한다. - 기능:
- 의존성 주입 요청을 처리한다.
- 인스턴스의 수명 주기를 유지한다 (예: 싱글톤, 팩토리).
- 런타임에서 모듈 로드 및 언로드를 관리한다.
get<T>()
또는inject<T>()
와 같은 객체를 검색하는 API를 제공한다.
- 요약:
Koin
은 애플리케이션이 실행되는 동안 의존성을 저장하고 해결하는 활성 DI 컨테이너이다.
실제 차이점
KoinApplication
: Koin 환경의 초기화자 및 _관리자_이다. Koin을 구성하고 모듈을 로드하며 DI 시스템을 설정Koin
: Koin DI의 _엔진_으로, 실제로 앱의 의존성의 수명 주기와 해결을 처리한다.
비유
- Koin을 공장으로 생각해보자:
KoinApplication
은 _공장 설정_이다. 여기서 청사진 (모듈)이 로드되고 조립 라인이 구성된다.Koin
은 공장 자체이며, 필요할 때 실제 객체 (의존성)를 _생산_하고 _제공_하는 역할을 한다.
koin 의 activityViewModel vs androidx 의 activityViewModels
PokemonDetailSkillFragment 에서
import androidx.fragment.app.activityViewModels
private val activityViewModel: PokemonDetailViewModel by activityViewModels()
를(koin 의 activityViewModel) 써도 제대로 동작하고,
import org.koin.androidx.viewmodel.ext.android.activityViewModel
private val activityViewModel: PokemonDetailViewModel by activityViewModel<PokemonDetailViewModel>()
를(ViewModelProvider. Factory 에 직접 접근하는 FragmentViewModelLazy.kt 의 activityViewModel) 써도 제대로 동작해.
무슨 차이가 있고 어떤 것을 더 추천하니?
Great question! Both of these approaches—using activityViewModels()
from Android’s Fragment
API
and activityViewModel()
from Koin—achieve the same result, which is sharing a ViewModel
between a fragment and its
parent activity. However, there are some key differences between them, especially considering their usage within the
context of Koin and Android's native mechanisms. Let's break it down.
1. AndroidX의 activityViewModels()
import androidx.fragment.app.activityViewModels
private val activityViewModel: PokemonDetailViewModel by activityViewModels()
- What it does: This is part of the AndroidX
ViewModel
library. It retrieves aViewModel
scoped to
theActivity
. If theActivity
has already created theViewModel
, it will return the same instance to the
fragment. Otherwise, it will create theViewModel
using the defaultViewModelProvider
unless you pass a custom
factory. - How it works: The
ViewModel
will be retained across configuration changes (like screen rotations), and the
fragment will share the sameViewModel
instance with the activity. - Default factory behavior: If you don't provide a factory explicitly,
activityViewModels()
will use the
defaultViewModelProvider
factory, which means it expects yourViewModel
to have an empty constructor or one that
is automatically handled bySavedStateHandle
if usingViewModel
with state. - Manual Factory: If your
ViewModel
requires dependencies (likeDexRepository
), you need to manually provide a
factory, which can make dependency injection with Koin less convenient. - 무엇을 하는가?: 이것은 AndroidX
ViewModel
라이브러리의 일부이다.Activity
에 대한 범위가 지정된ViewModel
을 검색한다.Activity
가
이미ViewModel
을 생성했다면, 동일한 인스턴스를 프래그먼트에 반환한다. 그렇지 않으면 기존의ViewModelProvider
를 사용하여ViewModel
을 생성한다. - 작동 방식:
ViewModel
은 구성 변경(화면 회전과 같은)을 통해 유지되며, 프래그먼트는 활동과 동일한ViewModel
인스턴스를 공유한다. - 기본 팩토리 동작: 명시적으로 팩토리를 제공하지 않으면,
activityViewModels()
는 기본ViewModelProvider
팩토리를 사용한다. 즉,ViewModel
이 빈 생성자를
가져야 하거나ViewModel
을 상태와 함께 사용할 때SavedStateHandle
에 의해 자동으로 처리되어야 한다. - 수동 팩토리:
ViewModel
에 의존성(예:DexRepository
)이 필요한 경우, 의존성 주입이 덜 편리해질 수 있으므로 수동으로 팩토리를 제공해야 한다.
2. activityViewModel()
from Koin
2. Koin의 activityViewModel()
import org.koin.androidx.viewmodel.ext.android.activityViewModel
private val activityViewModel: PokemonDetailViewModel by activityViewModel<PokemonDetailViewModel>()
- 무엇을 하는가?: 이것은 Koin의 확장 함수로, 활동에 범위가 지정된
ViewModel
을 검색한다.activityViewModels()
와 유사하지만, Koin의 의존성 주입 시스템을 통합한다. - 작동 방식: Koin은
PokemonDetailViewModel
에 선언된대로 의존성을 자동으로 생성하고 주입한다. - 수동 팩토리가 필요 없음: Koin은 의존성 주입을 처리하므로 수동으로
ViewModelFactory
를 제공할 필요가 없다. 특히,ViewModel
에 리포지토리, 로거 등과 같은 생성자 인수가
있는 경우 유용하다.
Key Differences
Aspect | activityViewModels() (AndroidX) |
activityViewModel() (Koin) |
---|---|---|
Dependency Injection | No built-in support, you need to use a ViewModelProvider.Factory if dependencies are needed. |
Automatically resolves dependencies using Koin’s DI. |
Factory Setup | Requires manual handling for complex ViewModels (e.g., with constructor parameters). |
Koin handles this via the DI module, making it easier to inject dependencies. |
Configuration | Standard Android API, no external libraries needed. | Requires Koin to be set up for the project. |
Recommendation | More flexible if you're not using a DI framework. | Better if you're already using Koin for DI, simplifies ViewModel creation. |
주요한 차이
Aspect | activityViewModels() (AndroidX) |
activityViewModel() (Koin) |
---|---|---|
DI | ViewModelProvider.Factory 를 사용할 필요가 있다. built-in 지원이 없다 |
koin 의 di 를 사용해서 자동으로 의존성 해결 |
팩토리 설정 | ||
Which One Should You Use?
Recommendation: Use activityViewModel()
from Koin
- If you're using Koin for dependency injection, I strongly recommend sticking with
activityViewModel()
from Koin.
It allows you to leverage Koin's DI framework seamlessly, and you don’t have to worry about
managingViewModelProvider.Factory
manually. - Cleaner code: You declare the dependencies in the Koin module (as you did in
viewModelModule
), and Koin
automatically provides thePokemonDetailViewModel
with the necessaryDexRepository
andAnalyticsLogger
injected. - Less boilerplate: With
activityViewModel()
, Koin handles everything under the hood, making your code cleaner,
especially forViewModels
with dependencies.
When to Use activityViewModels()
:
- No DI framework: If you're not using any dependency injection framework (like Koin or Dagger), you would
useactivityViewModels()
and manually provide aViewModelFactory
for complexViewModels
with dependencies. - Standard Android projects: If you're building a small project and don’t want to introduce a DI
framework,activityViewModels()
works fine, but you’ll need to manage the factory for dependency injection.
Example of DI with Koin:
Here’s how Koin simplifies things. Instead of worrying about how to provide the DexRepository
and AnalyticsLogger
:
val viewModelModule = module {
viewModelOf(::PokemonDetailViewModel) // Koin will resolve the dependencies
}
And in the fragment:
private val activityViewModel: PokemonDetailViewModel by activityViewModel()
Koin will take care of injecting DexRepository
and AnalyticsLogger
as declared in your module.
결론
- Koin 사용자:
activityViewModel()
을 사용하라. Koin의 DI와 잘 통합되며, 의존성을 처리하고 코드베이스를 단순화한다. - DI를 사용하지 않는 사용자: AndroidX의
activityViewModels()
를 사용하되,ViewModel
에 의존성이 있는 경우ViewModelProvider.Factory
를 수동으로 관리해야 한다.
당신의 경우, 이미 Koin을 사용하고 있으므로 activityViewModel()
을 사용하라. 프로젝트의 나머지 부분과 일관성이 있으며, 코드를 더 간단하게 유지할 수 있다.