BaseActivity, BaseFragment 만들 때의 제네릭

홍성덕·2024년 8월 1일

BaseActivity, BaseFragment를 만들 때 제네릭을 사용하였는데, 제네릭을 사용하게 된 과정을 글로 남기고 싶어서 작성했다. 이 글에서는 BaseFragment 코드를 예시로 가져왔다.
기존의 BaseFragment와 하위 클래스는 아래와 같이 작성되어 있었다.

// BaseFragment.kt
abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) {
	//...

    abstract fun bindView(view: View)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bindView(view)
    }
}

// FavoriteFragment.kt
class FavoriteFragment : BaseFragment(R.layout.fragment_favorite) {

    private var binding: FragmentFavoriteBinding? = null

    override fun bindView(view: View) {
        binding = FragmentFavoriteBinding.bind(view)
    }
    
    //...

추상 메서드 bindView()를 override함으로써 각각의 하위클래스에서 view를 bind해주는 코드가 작성되어 있었다. 그런데 나는 binding 변수와 view를 bind해주는 코드를 아예 BaseFragment에서 작성되도록 하고 싶었다.

맨처음에는 생성자로 binding 변수를 넘기는 것을 생각했다.

// BaseFragment.kt
abstract class BaseFragment(@LayoutRes layoutId: Int, private val binding: ViewBinding) : Fragment(layoutId)

// FavoriteFragment.kt
class FavoriteFragment : BaseFragment(R.layout.fragment_favorite, FragmentFavoriteBinding.bind()) // 불가능

근데 이렇게 하는 것은 불가능하다. BaseFragment의 생성자 파라미터로 선언된 private val binding: ViewBindingViewBinding 타입의 생성자로 ViewBinding은 interface이다. 그러면 interface를 구현한 클래스의 객체를 argument로 넣어줘야 한다.

하지만 inflate() 혹은 bind() 메서드를 이용해서 ViewBinding 객체(여기서는 FragmentFavoriteBinding 객체)를 넘겨야 하는데 이것을 FavoriteFragment를 생성하는 시점에서는 불가능하다.


그래서 이 문제를 해결책은 제네릭을 이용하는 것이다.

// 수정 후 코드

// BaseFragment.kt
abstract class BaseFragment<VB : ViewBinding>(
    @LayoutRes layoutId: Int,
    private val binder: (View) -> VB
) : Fragment(layoutId) {

    protected var binding: VB? = null

    //...

    final override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = super.onCreateView(inflater, container, savedInstanceState)
        if (view != null) {
            binding = binder(view)
        }
        return view
    }

    override fun onDestroyView() {
        binding = null
        super.onDestroyView()
    }
}

// FavoriteFragment.kt
class FavoriteFragment : BaseFragment<FragmentFavoriteBinding>(
    layoutId = R.layout.fragment_favorite,
    binder = FragmentFavoriteBinding::bind
)

제네릭을 사용하는 목적은 클래스 내부에서 사용할 데이터 타입(자료형)을 인스턴스를 생성할 때 확정하기 위해서이다.
위의 예시를 통해서 설명하면, FavoriteFragment 인스턴스를 생성할 때 내부에서 사용할 데이터 타입을 FragmentFavoriteBinding로 확정하기 위해서이다. 이렇게 한다면 FavoriteFragment 인스턴스를 생성할 때 뿐만 아니라, 다른 Fragment 인스턴스를 생성할 때도 각 FragmentBinding으로 데이터 타입을 확정할 수 있다.

또한 추가적인 제네릭의 기능으로 클래스나 메서드에서 사용할 데이터 타입을 컴파일 타임에 검사하여 데이터 타입을 제한한다는 기능이 있다. 위의 예시에서는 <VB : ViewBinding>을 통해 VB의 데이터 타입을 ViewBinding 타입으로 제한한다.


예시로는 BaseFragment를 가져왔지만, BaseActivity도 비슷하게 제네릭을 이용해서 작성 가능하다.
추후 제네릭에 대한 전반적인 정리에 대한 글도 작성할 예정이다.


참고자료

profile
안드로이드 주니어 개발자

0개의 댓글