Android/Development Tips

Custom view 에서 Koin 사용해 ViewModel 주입할 때 주의 할점

Nanamare 2019. 8. 5. 23:39
728x90

몇일전에 CustomView에서 viewModel 을 주입하며 삽질한 경험입니다.

보통 커스텀 뷰를 만들면 이런 모습이 많이 나오게 됩니다.

class CustomView
    : BaseCustomView<CustomViewBinding> {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

}

근데 만들고 나면 CustomViewModel 을 주입 시켜줘야 하는데 액티비티가 아니기 때문에 by inject() 함수를 사용해 주입해줄 수 없다.

그럴때 간단하게 KoinComponent 를 implement 해주면 된다 그럼 by inject() 를 사용해 주입시킬 수 있다.

class CustomView
    : BaseCustomView<CustomViewBinding>, KoinComponent {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    
    val customViewModel : CustomViewModel by inject()

    // 뷰모델 초기화
    override fun setTypeArray(typedArray: TypedArray) {
         binding.run {
            vm = customViewModel
        }
    }
}

하지만! 이때 by inject() 를 해줘도 customViewModel 의 getValue 가 null 이 나오는 상황이 있다.
보통 이런 실수는 클래스의 생성자에서 viewModel 을 주입하기 때문이다.
-> 문제의 원인을 생각해보면 setTypeArray() 함수가 너무 빠르게 시작되어, 아직 만들어지지않은 CustomViewModel 을 참조하기 때문이다.

보통 부모 클래스를 아래와 같은식으로 코딩해서, initView() 나 setTypeArray() 같은 함수에서 데이터 바인딩을 사용하여 초기화를 해주는 코드가 많은데 Koin을 사용하면, 데이터 바인딩을 사용하여 초기화를 해주는 시점을 뒤로 미뤄야한다.

abstract class BaseCustomView<B : ViewDataBinding>
    : FrameLayout {
    constructor(context: Context) : super(context) {
        initView()
    }
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        initView()
        getAttrs(attrs)
    }
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        initView()
        getAttrs(attrs, defStyleAttr)
    }
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
        initView()
        getAttrs(attrs, defStyleAttr, defStyleRes)
    }
    private fun getAttrs(attrs: AttributeSet?) {
        setTypeArray(context.obtainStyledAttributes(attrs, getCustomViewStyle()))
    }
    private fun getAttrs(attrs: AttributeSet?, defStyle: Int, defStyleRes: Int = 0) {
        setTypeArray(context.obtainStyledAttributes(attrs, getCustomViewStyle(), defStyle, defStyleRes))
    }
    private fun initView() {
        binding = DataBindingUtil.bind(LayoutInflater.from(context)
                .inflate(getLayoutId(), this@BaseCustomView, false).apply {
                    addView(this)
                })!!
    }
    abstract fun setTypeArray(typedArray: TypedArray)
    abstract fun getLayoutId(): Int
    abstract fun getCustomViewStyle(): IntArray
    protected lateinit var binding: B
}

우리는 생성자의 호출이 끝나고, 화면에 커스텀 뷰가 추가되는 시점인 onAttachedToWindow 에서 초기화를 진행해주면 된다.

아래와 같이 코딩하면 아주 잘 작동한다!

 override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        binding.run {
            vm = customViewModel

        }
}
728x90