안녕하세요 점냥입니다 :)
AAC ViewModel 즉, ViewModel에서 Context를 잘못 사용할 경우 메모리 측면에서 안 좋은 영향을 준다는 것을 알고 있어서 최대한 안 쓰는 방향으로 개발을 하고 있어요. 하지만 종종 Context의 필요성을 조금씩 느끼고 있는데요. 그럴 때마다 내가 잘못 알고 있는 것은 아닌 지, Context를 좀 더 잘 사용하는 방법이 무엇인지 고민을 하게 되었어요.
ViewModel에서 Context가 필요한 상황은
class MyViewModel: ViewModel() {
fun getA() {
if (...) throw CustomException("이건 잘못된 상황이야!")
...
}
}
Android에서는 HardCoded String으로 선언된 문자열을 strings.xml에서 관리하도록 권장하고 있어요. Context와 밀접한 Android 객체에서는 Context의 getString(resourceId: Int) 함수를 쉽게 접근할 수 있어 문제가 없지만 Android와 거리가 먼 ViewModel, Data Layer에서는 Context가 없기 때문에 쉽지 않더라고요.
차선책으로 Enum에 생성자로 String Resouce Id를 지정해서 LiveData 등의 뷰가 관찰 가능한 데이터 흐름으로 변환하여 뷰에서 Context로 String을 가져오는 방식이 있어요. 하지만 이 방법은 개인적으로 저에게 굉장히 돌아가는 느낌을 주더라고요. 처음에는 String을 관리하는 작은 과제였지만 이것을 위해 View에서 observe 등으로 ViewModel을 관찰하게 변경하고 이러한 패턴이 반복되었을 때 base class로 분리하는 등 배보다 배꼽이 더 큰 느낌을 받았어요.
ViewModel에서 Activity Context 사용은 X
Context가 필요하다 해서 Activity Context를 ViewModel에서 사용하면 안 돼요. ViewModel은 화면 회전 등으로 Activity가 죽었다가 다시 생성될 때도 이전과 동일한 데이터를 유지하기 위해서 수명이 길게 설계가 되었어요. Activity의 메모리가 해제되고 나서 ViewModel 메모리도 해제가 되지만, Activity Context를 ViewModel이 참조하고 있을 경우 순한 참조가 되어버려서 메모리 릭이 발생할 수 있어요
Answer 1. AndroidViewModel 사용 혹은 Application Context DI로 주입
public class AndroidViewModel extends ViewModel {
@SuppressLint("StaticFieldLeak")
private Application mApplication;
public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}
/**
* Return the application.
*/
@SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
@NonNull
public <T extends Application> T getApplication() {
return (T) mApplication;
}
}
AndroidViewModel은 Android에서 Activity Context 대신 Application Context를 사용하라는 측면에서 만들어준 클래스예요. Android Application Class를 생성자로 받아서 내부에 변수로 저장하고 Context가 필요할 때 저장한 Application 객체에서 applicationContext 변수를 접근하면 돼요!
@HiltViewModel
class MyViewModel @Inject constructor(
@ApplicationContext context: Context
) { ... }
비슷한 방법으로 Hilt를 사용해 DI를 사용하고 있다면 @ApplicationContext를 통해 Context를 주입해줄 수도 있어요
Application Context는 App 프로세스가 실행될 때 만들어지고 프로세스가 종료될 때까지 유지되는 객체이기 때문에 ViewModel 보다 수명이 길어 앞서 Activity Context에서 우려했던 메모리 릭 문제가 발생하지 않아요
하지만 단점이 있어요.
ViewModel Unit Test를 해야 해요
Android에서 Unit Test의 주 대상은 ViewModel인데요. 위처럼 Application 혹은 Context를 ViewModel에 직접 주입해줄 경우 단위 테스트 환경에서는 Android 라이브러리에 직접 접근을 못하기 때문에 문제점이 발생해요. 또 이를 위해 Context 자체를 Mock 객체를 만들어주는 라이브러리를 추가해주기도 하는데요. 근본적인 원인을 해결하지 않고 타 라이브러리에 기대어 문제를 해결한다는 것이 아쉽더라고요.
Answer 2. Provider 혹은 Delegator 만들어주기
class ResourceProviderImpl @Inject constructor(
@ApplicationContext context: Context
): ResourceProvider {
override fun getString(@StringRes resourceId: Int) = context.getString(resourceId)
}
다른 방법으로는 String을 제공해주는 Provider 또는 Delegator를 만드는 방법이에요.
@HiltViewModel
class MyViewModel @Inject constructor(
private val resourceProvider: ResourceProvider
) : ViewModel() { ... }
이 방법은 Application Context를 주입해주는 것은 동일하지만 오직 ViewModel 관점에서 바라보면 Context에 대한 직접 참조가 사라지고 순수 Kotlin으로 작성된 Interface를 주입받기 때문에 Unit Test에서도 걱정 없이 사용 가능해요. 원한다면 Fake 객체를 만들어 테스트를 알맞게 구성할 수도 있겠죠?
'Android > Common' 카테고리의 다른 글
[Android] 디버그 앱, 출시 앱 분리하기 (4) | 2022.11.12 |
---|---|
[Android] Flow 흐름을 turbine으로 쉽게 테스트 코드 작성해보기 (2) | 2022.10.27 |
[Android] EventBus (0) | 2022.08.28 |
[Android] Bottom Navigation Bar State with Compose - (2) (0) | 2022.06.12 |
[Android] ViewTree 내에 동일한 ID을 가진 위젯이 존재할 경우 UI 상태 저장 오류 해결 방법 - 2 (0) | 2022.05.13 |