본문 바로가기

안드로이드

Android Data Binding을 활용한 ViewPager와 TabLayout 연결

안드로이드 개발하면서 화면 구성에 TabLayout을 자주 사용하게 된다. 또한 여러 개의 뷰를 좌우로 넘기면서 확인할 수 있는 ViewPager도 사용한다. Fragment 구성을 하면 자주 쓰는 두 Layout을 서로 연결하는 방법을 알아보도록 하겠다.

 

방법을 간단히 설명하면, ViewPager의 Listener 중 OnPageChangeListener가 있는데 메서드가 3개 있다.

 

onPageScrollStateChanged(state: Int)

ViewPager의 스크롤 상태가 변경될 때 호출되는 메서드며, 총 3가지 상태가 있다.

  • SCROLL_STATE_IDLE : Indicates that the pager is in an idle, settled state. The current page is fully in view and no animation is in progress.
  • SCROLL_STATE_DRAGGING : Indicates that the pager is currently being dragged by the user.
  • SCROLL_STATE_SETTLING : Indicates that the pager is in the process of settling to a final position.

onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int)

onPageSelected(position: Int)

 

먼저 DataBinding을 사용하기 위해 build.gradle(app)에 아래의 코드를 추가해준다.

android {
...
    dataBinding {
        enabled = true
    }
...
}

 

ViewPager를 사용하기 위해 FragmentStatePagerAdapter를 상속해 PagerAdapter를 생성한다.

Fragment를 다루는 PagerAdapter는 FragmentStatePagerAdapter와 FragmentPagerAdapter가 있는데 두 클래스의 차이점은 FragmentPagerAdapter의 경우 생성된 Fragment를 메모리에 담고 있기 때문에 리소스 사용이 그만큼 늘어나게 되고, FragmentStatePagerAdapter는 사용자에게 보여지는(곧 보여질 Fragment 포함) 몇 개의 상태만 유지하고 나머지 Fragment는 메모리에서 제거한다. 즉 View를 제거하는 것이다. 상황에 맞게 적절한 PagerAdapter를 사용하는 것이 중요하다.

...
class ViewPagerAdapter(fragmentManager: FragmentManager) :
    FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    override fun getItem(position: Int): Fragment {
        return when (position) {
            0 -> FragmentA()
            1 -> FragmentB()
            else -> FragmentC()
        }
    }

    override fun getCount() = 3
}

 

BindingAdapter 구현하여 바인딩에 필요한 메서드 생성한다.

setTapContents() : TabLayout 구현 및 리스너 생성

setViewPager() : ViewPager에 ViewPagerAdapter 지정 및 리스너 생성

setViewPosition()

TabLayout 및 ViewPager 모두 position 값을 가져올 수 있기 때문에 해당 포지션(사용자가 선택한 위치)을 ViewModel로 전달하여 해당 값으로 TabLayout 및 ViewPager의 포지션을 지정해 줄 수 있다.

...
object BindingAdapter {
...
    @BindingAdapter("setTapContents", "setVm")
    @JvmStatic
    fun setTapContents(tabLayout: TabLayout, items: List<String>?, mainVm: MainViewModel?) {
        items?.forEach {
        	with(tabLayout) {
            	newTab().let { tab ->
                	tab.text = it
                    addTab(tab)
                }
                addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
                  override fun onTabReselected(tab: TabLayout.Tab?) {
                      //Nothing.
                  }

                  override fun onTabUnselected(tab: TabLayout.Tab?) {
                      //Nothing.
                  }

                  override fun onTabSelected(tab: TabLayout.Tab?) {
                      tab?.position?.let { position ->
                          mainVm?.selectPosition(position)
                      }
                  }
              })
            }
        }
    }

    @BindingAdapter("setFsm", "setVm")
    @JvmStatic
    fun setViewPager(
        viewPager: ViewPager,
        fragmentManager: FragmentManager?,
        mainVm: MainViewModel?
    ) {
        if (!items.isNullOrEmpty())
            viewPager.adapter?.run {
                if (this is ViewPagerAdapter) {
                    //do something.
                }
            } ?: kotlin.run {
                if (fragmentManager != null)
                    viewPager.adapter = ViewPagerAdapter(fragmentManager, items)
                viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
                    override fun onPageScrollStateChanged(state: Int) {
                        //Nothing.
                    }

                    override fun onPageScrolled(
                        position: Int,
                        positionOffset: Float,
                        positionOffsetPixels: Int
                    ) {
                        //Nothing.
                    }

                    override fun onPageSelected(position: Int) {
                        mainVm?.selectPosition(position)
                    }
                })
            }
    }
    
    @BindingAdapter("setViewPosition")
    @JvmStatic
    fun setViewPosition(view: View, position: Int?) {
        if (position != null)
            when (view) {
                is ViewPager -> {
                    view.currentItem = position
                }
                is TabLayout -> {
                    view.run {
                        getTabAt(position)?.let { tab ->
                            selectTab(tab)
                        }

                    }
                }
            }
    }
...
}

 

ViewModel을 생성한다.

tabItems : tablayout의 Name

position : 사용자가 선택한 혹은 보여지는 View의 position

...
class MainViewModel : ViewModel() {
    val tabItems: LiveData<List<String>> get() = _tabItems
    val position: LiveData<Int> get() = _position

    private val _tabItems: MutableLiveData<List<String>> = MutableLiveData()
    private val _position: MutableLiveData<Int> = MutableLiveData()

    companion object {
        private val TAB_ITEMS = listOf(/** Tab Name */)
    }

    init {
        _tabItems.postValue(TAB_ITEMS)
    }

    fun selectPosition(position: Int) {
        _position.postValue(position)
    }
}

 

생성한 ViewModel을 Activity에서 바인딩한다.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).run {
            mainVm = ViewModelProvider.NewInstanceFactory().create(MainViewModel::class.java)
            fragmentManager = supportFragmentManager
            lifecycleOwner = this@MainActivity
        }
    }
}

 

이제 layout.xml에 바인딩을 하여 마무리해주면 된다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="mainVm"
            type="com.kobbi.project.coronamask.ui.viewmodel.MainViewModel" />

        <variable
            name="fragmentManager"
            type="androidx.fragment.app.FragmentManager" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.activity.MainActivity">

       

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/lo_tab"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:setTapContents="@{mainVm.tabItems}"
            app:setViewPosition="@{mainVm.position}"
            app:setVm="@{mainVm}"
            app:tabGravity="center"
            app:tabMode="auto" />

        <androidx.viewpager.widget.ViewPager
            android:id="@+id/view_pager"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toTopOf="@id/lo_tab"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:setFsm="@{fragmentManager}"
            app:setPagerCount="@{mainVm.tabItems}"
            app:setViewPosition="@{mainVm.position}"
            app:setVm="@{mainVm}" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>