본문 바로가기

카테고리 없음

[Android] ViewModel(with Hilt) init 함수 동작 문제

ViewModel의 init 함수에 동작을 설정하고 Activity에서 호출하여 작동을 확인하고 있었다.

의도한 동작을 하지 않아 로그를 찍어보니 init 함수가 동작을 하지 않는 문제를 확인했다.

 

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SomethingComposable {
                Button(
                    onClick = {
                        viewModel.update(SOME_STRING)
                    }
                )
            }
        }
    }
}

 

@HiltViewModel
class MainViewModel @Inject constructor(
    private val fooRepository: FooRepository
): ViewModel() {

    private val _sth = MutableStateFlow<List<String>>(listOf())
    val sth = _sth.toStateFlow()

    init {
        Log.e("URGENT_TAG", "MainViewModel init", ) // <- 동작 안함
        viewModelScope.launch {
            fooRepository.getSth().collectLatest {
            	_sth.emit(it)
            }
        }
    }
    
    fun update(elem: String) {
    	val temp = _sth.value.toMutableList()
        temp.add(elem)
        updateSth(temp)
    }
    
    private fun updateSth(list: List<String>) = fooRepository.updateSth(list)

 

위 코드들에서 먼저 확인해 봐야 하는 것은 by 키워드이다.

by는 lazy delegate를 수행한다. 즉, 참조하기 전에 생성자가 수행되지 않는다.

 

위 코드는 데이터를 업데이트하고 db의 트리거를 통해 값을 전달 받는 의도로 작성 되었다.

하지만 엑티비티단에서 위와 같이 작성하는 경우 최초로 값을 읽는 동작과  값을 읽는 동작이

병렬적으로 실행된다.

 

따라서, stateflow의 initial 값으로 설정한

빈 리스트에 하나의 스트링이 추가된 값이 db에 업데이트가 되고 해당 값을 읽어오게 되면서

스트링 값들을 추가로 저장하는 역할을 수행하지 못하게 된다.

 

 

이에 대한 해결책들은 다음과 같다.

 

# 쓰기와 읽기가 병렬적으로 수행되지 못하도록 한다.

위 방법을 구현하기 위해서는 Mutex를 이용한 방법과

update 함수를 수행할때 db의 값을 읽어온 후에 해당 값을 이용하여 update하는 코루틴을 구성하는 방법이 있겠다.

 

# Activity에 ViewModel을 선언할 때 Lazy 키워드를 사용하지 않는다.

위 방법을 구현하면서 Lifecycle, singleton을 보장하려면 viewModelProvider를 사용하는 방법이 있다.

 

지금 문제가 발생하는 이유는 db에서 값을 읽기전에 stateflow의 초기값으로 설정된 빈 리스트이다.

따라서, 해당 stateflow가 값을 잘 읽어오면 문제가 해결될 것 이므로 2번째 방법으로 수행하는 것이 좋겠다.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel = ViewModelProvider(this)[MainViewModel::class.java]
   
        setContent {
            SomethingComposable {
                Button(
                    onClick = {
                        viewModel.update(SOME_STRING)
                    }
                )
            }
        }
    }
}

 

위 코드와 같이 수정한 후로 의도대로 동작하는 것을 확인했다.

 

 

https://stackoverflow.com/questions/66839757/init-is-not-called-when-injecting-viewmodel-using-hilt

 

init is not called when injecting viewModel using Hilt

I want to make API request when ViewModel is initialized. That`s why I make API request inside init method; expecting init be triggered when I inject viewModel in Activity. What am I doing wrong?

stackoverflow.com

 

https://dagger.dev/hilt/view-model.html

 

View Models

Note: Examples on this page assume usage of the Gradle plugin. If you are not using the plugin, please read this page for details. Hilt View Models A Hilt View Model is a Jetpack ViewModel that is constructor injected by Hilt. To enable injection of a View

dagger.dev

 

by viewModels() 를 통하여 viewModel을 선언하는 것은 KTX 라이브러리의 함수로

hilt viewmodel에서 권장 되는 방법이 아는 것을 확인 할 수 있었다.

 

Dagger 공식 문서에서는 

Warning: Even though the view model has an @Inject constructor, it is an error to request it from Dagger directly (for example, via field injection) since that would result in multiple instances. View Models must be retrieved through the ViewModelProvider API. This is checked at compile time by Hilt.

 

반드시 ViewModelProvider를 사용하라는 경고 문구도 있다.