Android ViewModel에 대한 매개변수 주입

ViewModel에 종속성을 주입하는 것은 이미 좋은 사례이며 구현을 유연하고 쉽게 테스트할 수 있도록 합니다.

그러나 화면이나 Fragment에 제공되는 매개변수는 어떻습니까? 예를 들어 Fragment Args 또는 Compose 탐색 매개변수가 있습니다. View에서 매개변수를 수신하고 ViewModel을 설정하는 데 종종 init 메소드와 같은 것이 사용됩니다. 이것은 우리가 알아야 할 ViewModel에 추가 단계를 추가합니다. 따라서 종속성뿐만 아니라 생성자의 매개변수도 가져오는 것이 더 유리할 것입니다.

설정



이 예제에서는 단순하게 유지하고 주로 매개변수 처리에 중점을 둡니다.

두 개의 화면이 있는 앱을 만듭니다.
  • 화면 1은 버튼일 뿐입니다. 탭하면 난수를 얻고 화면 2로 이동하여 난수를 매개 변수로 전달합니다.


  • 화면 2는 난수를 수신하고 보기 상태를 만들고 결과를 텍스트로 표시합니다.



  • 화면은 Jetpack Compose로 생성되며 예제에서는 탐색을 위해 Composes NavHost도 사용하지만 동일한 ViewModel 코드가 활동 및 프래그먼트 사용에 적용됩니다. 유일한 차이점은 매개변수로 사용할 수 있는 유형입니다. 다음 설정에서 볼 수 있는 Compose Navigation은 탐색 경로 문자열의 일부로 매개변수를 전달할 수만 있습니다.

    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "home
    ) {
        composable(route = "home") {
            HomeScreen {
                navController.navigate("details/$it")
            }
        }
        composable(route = "details/{randomNumber}") {
            val viewModel = viewModel<DetailsFlowViewModel>()
            DetailsScreen(viewModel = viewModel)
        }
    }
    


    보시다시피 두 번째 화면에는 randomNumber 매개변수를 선언하는 경로 세부정보/{randomNumber}가 있습니다.

    저장된 상태 처리



    이제 중요한 질문입니다. 탐색 후 두 번째 화면에서 ViewModel의 매개변수를 어떻게 검색할 수 있습니까?

    SavedStateHandle 클래스에는 필요한 정보가 포함되어 있으며 ViewModel의 생성자에 직접 주입할 수 있습니다.

    class DetailsFlowViewModel(
        savedStateHandle: SavedStateHandle
    ) : ViewModel() {
        ...
    }
    


    이것은 Hilt와 같은 종속성 주입 프레임워크의 도움이 있든 없든 가능합니다.

    SavedStateHandle은 매개변수에 도달하는 두 가지 방법을 제공합니다.

    operator fun <T> get(key: String): T?
    



    fun <T> getStateFlow(key: String, initialValue: T): StateFlow<T>
    


    달성하려는 목표에 따라 두 가지 방법 중 하나를 사용할 수 있습니다. 우리의 경우 ViewModel에서 UI로의 View State 흐름을 제공하고 싶으므로 getStateFlow를 사용하겠습니다.

    class DetailsFlowViewModel(
        savedStateHandle: SavedStateHandle
    ) : ViewModel() {
        val state: Flow<DetailsState> = savedStateHandle
            .getStateFlow<String?>("randomNumber", null)
            .map {
                val number = it?.toIntOrNull() ?: throw IllegalArgumentException("You have to provide randomNumber as parameter of type Int when navigating to details")
    
                // call dependencies as needed
                val result = "Fancy processing: $number"
                DetailsState(result)
            }
    }
    


    Important: Since we are using Compose Navigation we first have to retrieve the parameter as a String before we can convert it to its actual type Int. With Fragment Args it would be possible to directly get the parameter as an Int.



    한 걸음 더 나아가



    우리는 이미 매개변수를 ViewModels 생성자에 직접 제공할 수 있습니다. 그러나 여전히 단점이 있습니다. ViewModel 생성자는 원하는 것을 정확히 알려주지 않습니다. 테스트에서 생성자에 전달하기 전에 String 유형의 randomNumber를 SavedStateHandle로 설정하는 방법을 알아야 합니다. 구현 세부 사항에 대한 많은 지식이 필요한 것 같습니다.

    생성자가 다음과 같이 알려주면 더 좋지 않을까요? I want to have the parameter randomNumber of type Int.

    Hilt와 같은 종속성 주입 프레임워크의 도움으로 이를 달성할 수 있습니다.

    To keep it short I'm not going in to details on the basic usage of Hilt in this post. In case you want to read up on Hilt you can go to its Android Developers tutorial



    먼저 Qualifier 주석을 생성하여 Hilt에 대한 매개변수를 식별할 수 있습니다.

    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class RandomNumber
    


    Qualifier RandomNumber를 사용하여 ViewModel 범위에 매개변수를 제공하는 작은 Hilt 모듈을 만들 수 있습니다.

    @Module
    @InstallIn(ViewModelComponent::class)
    object DetailsModule {
        @Provides
        @RandomNumber,
        @ViewModelScoped
        fun provideRandomNumber(savedStateHandle: SavedStateHandle): Int =
            savedStateHandle.get<String>("randomNumber")?.toIntOrNull()
                ?: throw IllegalArgumentException("You have to provide randomNumber as parameter with type Int when navigating to details")
    }
    


    ViewModelComponent에 모듈을 설치하여 모듈이 주입된 ViewModel의 수명 동안 매개변수를 사용할 수 있도록 합니다. 실제 provideRandomNumber 메서드는 기본적으로 이전에 ViewModel에 있었던 코드와 같지만 한 가지 차이점이 있습니다. Flow를 사용하지 않고 값을 직접 가져옵니다.

    모듈을 사용하면 ViewModel이 정말 간단해집니다.

    @HiltViewModel
    class DetailsHiltViewModel @Inject constructor(
        @RandomNumber randomNumber: Int
    ) : ViewModel() {
        override val state: Flow<DetailsState> = flow {
            // call dependencies as needed
            val result = "Fancy processing: $randomNumber"
            emit(DetailsState(result))
        }
    }
    


    Qualifier를 사용하여 원하는 매개변수를 요청하고 이를 사용하여 View State를 생성합니다.

    결론



    이 게시물에 표시된 것처럼 매개변수 주입을 사용하면 SavedStateHandle을 주입하거나 초기화 메서드를 생성하는 것보다 약간 더 많은 코드가 필요하지만 앱의 다양한 측면을 더 잘 분리하여 더 읽기 쉽고 테스트 가능한 코드를 허용합니다.

    SavedStateHandle, Hilt 및 활동을 사용하는 다양한 변형이 있는 전체 예는 GitHub에서 찾을 수 있습니다.

    궁금한 점이 있다면 Koin을 사용해도 동일한 개념을 얻을 수 있습니다.

    다음편에서 만나요👋

    좋은 웹페이지 즐겨찾기