본문 바로가기
Android/Error Report

[Android] Kotlin android extensions에서 View binding으로 전환

by 굿햄 2023. 3. 8.

오늘에서야 Kotlin 1.7.21을 떠나보내고, 성능이 개선된 1.8.0으로 업그레이드하려던 찰나

Kotlin android extension 플러그인을 더 이상 지원하지않는다는 에러가 나에게 찾아왔다.

The 'kotlin-android-extensions' Gradle plugin is no longer supported. Please use this migration guide (https://goo.gle/kotlin-android-extensions-deprecation) to start working with View Binding (https://developer.android.com/topic/libraries/view-binding) and the 'kotlin-parcelize' plugin.

기존 KTX는 kotlin에서만 사용할 수 있는 점과, 지저분한 Global Namespace로 인해 동일한 뷰 ID가 있을 경우 실수할 여지가 존재하는 등의 문제가 있었기에, View의 선언을 명확히 하고 실수들을 없애기 위해 View binding을 권장하기 시작했다.

관련 글귀는 여기에서 확인할 수 있다.

 

그 동안 경고 수준이였던 문구가 이제는 에러로 넘어온 것.

조치를 취해야 한다.

 

마이그레이션 과정은 자체는 어렵지 않다.

Kotlin Extension을 사용해서 뷰 ID로 선언했던 것을 View binding을 통해 각 뷰에 접근하도록 수정하면 해결된다.


View binding 활성화

우선 ViewBinding을 활성화 시켜주어야 한다.

각 모듈안에 build.gradle 파일 내에 아래 구문을 추가한다.

android {
    ...
    buildFeatures {
        viewBinding = true
    }
}

아마 지속적인 개발이 이루어졌다면 대부분 이미 추가되어있을 것이다.

 

KTX 제거 및 Parcelize 플러그인 추가

먼저 android.extension 플러그인을 제거한다.

그리고 @Parcelize 어노테이션을 사용하고 있다면 아래처럼 플러그인을 추가해준다.

plugins {
    // kotlin("android.extensions")
    id("kotlin-parcelize")
}

 

그리고 아래와 관련된 구문이 있다면 제거해준다.

androidExtensions {
    experimental = true
}

 

KTX를 View binding으로 변경

여기부터 매우 번거로운 작업인데, 기존에는 View ID를 통해 직접 뷰 객체에 접근하였다면, 그 뷰가 어느 레이아웃에 소속되었는지 선언해주어야 한다.

 

먼저 kotlin.android.synthetic 을 import한 곳을 찾아준다.

import kotlinx.android.synthetic.*

내가 근무하는 회사 플랫폼의 경우 100개가 넘게 사용되고 있었다.

언제 다 하지..

 

View binding을 하는 방법은 두 가지가 있다.

  • Binding class를 이용하는 방법
  • DataBindingUtil을 통한 binding 생성 방법

Activity나 Fragment나 생성하는 방법은 동일하다.

 

Binding class를 사용하는 경우

class SampleActivity: AppCompatActivity() {
    private lateinit var binding: ActivitySampleBinding
    private val viewModel: SampleViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySampleBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ...
    }
    
    private fun initClickEvent() {
        // change KTX -> View binding
        // val btnArray = arrayOf(btnSelfAdIntroSkip, btnSelfAdIntroClose)
        val btnArray = arrayOf(binding.btnSelfAdIntroSkip, binding.btnSelfAdIntroClose)
        btnArray.map { view ->
            view.setOnClickListener {
                finish()
            }
        }
    }

xml로 작성하면 자동으로 binding class를 생성해준다. (e.g. activity_sample.xml → ActivitySampleBinding.xml)

 

따라서 binding 클래스에 inflate 함수로 layoutinflater만 넘기면 바로 사용이 가능하고,

기존 ktx를 사용해서 접근하던 View ID앞에 binding 객체를 붙혀주면 된다.

 

DataBindingUtil을 사용하는 경우

특수한 케이스로 Binding 클래스 이름을 모르는 경우나 동적으로 레이아웃 ID를 파라미터로 전달받아 생성할 때 주로 사용한다.

// Case 1
val viewRoot = LayoutInflater.from(this).inflate(layoutId, container, isAttachToParent)
val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)

// Case 2
val binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, isAttachToParent)

이렇게 사용하면 ViewDataBinding 객체를 생성할 수 있다.

 

화자의 경우 Activity 및 Fragment의 Base 코드를 작성하여 binding 할 레이아웃 id를 파라미터로 전달받는다.

open class BaseFragment<T : ViewDataBinding> : Fragment() {
    private var _binding by autoCleared<T>()  // View 소멸 시 binding 제거
    open val binding: T
        get() = _binding
    open val resId: Int = 0

    fun getViewBinding(): T {
        return binding
    }

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

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        _binding = DataBindingUtil.inflate(inflater, resId, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        return binding.root
    }
    ...
}

위의 코드는 View Binding를 생명주기에 따라 적절한 라이프 사이클에 제거하기 위해 주로 사용되는 BaseFragment 코드이다.

레이아웃 내의 뷰에 접근하기위해 타입을 알아야 하므로 제네릭으로 Binding 변수의 타입도 함께 전달해야 한다.

class SampleFragment() : BaseFragment<FragmentSampleBinding>() {
    override val resId = R.layout.fragment_sample
    ...

 

 

ⓒ 굿햄 2022. daryeou@gmail.com all rights reserved.

댓글