MainActivity는 프레그먼트 전환에 사용될 ViewPager2와 3개의 TabItem을 가진 TabLayout으로 구성된다. MainActivity가 실행되면 우선 로그인 액티비티인 SignInActivity가 실행된다. SignActivity는 사용자의 정보를 포함하는 UserInfo클래스 데이터를 반환하고 이를 토대로 프레그먼트를 구성한다. 본문에서는 ViewPager2와 TabLayout을 연결하는 법과 그 과정에서 생긴 문제점을 정리하고자 한다.
1. 전체 코드
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/main_white"
app:layout_constraintBottom_toTopOf="@+id/main_tab"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/main_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/main_orange"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:tabIndicatorColor="@color/main_orange"
app:tabIndicatorGravity="stretch"
app:tabRippleColor="@android:color/transparent">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_baseline_chat_bubble_24" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_baseline_person_outline_24" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_baseline_folder_open_24" />
</com.google.android.material.tabs.TabLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package com.example.catting
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.example.catting.databinding.ActivityMainBinding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.google.gson.Gson
class MainActivity : AppCompatActivity() {
val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
lateinit var signInResult :ActivityResultLauncher<Intent>
lateinit var api: RetrofitApplication
var userInfo: UserInfo = UserInfo(null,null,null, arrayListOf<CatProfile>())
var mBackWait:Long = 0
init{
instance = this
}
companion object{
private var instance:MainActivity? = null
var isChattingFragmentNeedRefresh = true
var isUserInfoFragmentNeedRefresh = true
var isCatInfoFragmentNeedRefresh = true
fun getInstance(): MainActivity? {
return instance
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
api = RetrofitApplication.create()
signInResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if(it.resultCode == RESULT_OK) {
val data: Intent? = it.data
userInfo = data?.getParcelableExtra<UserInfo>("userInfo")!!
}
else if(it.resultCode == RESULT_FIRST_USER){
binding.mainTab.selectTab(binding.mainTab.getTabAt(1))
}
}
val intent = Intent(this@MainActivity, SignInActivity::class.java)
signInResult.launch(intent)
with(binding) {
val fragmentList =
listOf<Fragment>(ChattingFragment(), UserInfoFragment(), CatInfoFragment())
val mainPagerAdapter = MainFragmentPagerAdapter(fragmentList, this@MainActivity)
viewPager.adapter = mainPagerAdapter
val mainFragmentName = listOf(R.drawable.ic_baseline_chat_bubble_24,
R.drawable.ic_baseline_person_outline_24, R.drawable.ic_baseline_folder_open_24)
TabLayoutMediator(mainTab, viewPager){ tab, position->
tab.setIcon(mainFragmentName[position])
}.attach()
viewPager.isUserInputEnabled = false
mainTab.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener{
override fun onTabSelected(tab: TabLayout.Tab?) {
tab?.position?.let{viewPager.setCurrentItem(it, false)}
when(tab!!.position){
0->tab.setIcon(R.drawable.ic_baseline_chat_bubble_24)
1->tab.setIcon(R.drawable.ic_baseline_person_24)
2->tab.setIcon(R.drawable.ic_baseline_folder_24)
}
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
when(tab!!.position){
0->tab.setIcon(R.drawable.ic_baseline_chat_bubble_outline_24)
1->tab.setIcon(R.drawable.ic_baseline_person_outline_24)
2->tab.setIcon(R.drawable.ic_baseline_folder_open_24)
}
}
})
}
}
override fun onBackPressed() {
if(System.currentTimeMillis() - mBackWait >=2000 ) {
mBackWait = System.currentTimeMillis()
Snackbar.make(binding.viewPager,"뒤로가기 버튼을 한번 더 누르면 종료됩니다.",Snackbar.LENGTH_LONG).show()
} else {
finish()
}
}
fun openChattingActivity(catProfile: CatProfile) {
val intent = Intent(this@MainActivity, ChattingActivity::class.java).apply {
putLargeExtra("CatProfile",catProfile)
}
startActivity(intent)
}
fun openSettingActivity(){
val intent = Intent(this@MainActivity, SettingActivity::class.java)
startActivity(intent)
}
}
class MainFragmentPagerAdapter(val fragmentList: List<Fragment>, fragmentActivity: FragmentActivity)
: FragmentStateAdapter(fragmentActivity){
override fun getItemCount() = fragmentList.size
override fun createFragment(position: Int) = fragmentList[position]
}
2. 정리
- ViewPager2와 TabLayout 연결하기
...
with(binding) {
// 1. 페이지 데이터를 로드
val fragmentList =
listOf<Fragment>(ChattingFragment(), UserInfoFragment(), CatInfoFragment())
// 2. 어댑터 생성
val mainPagerAdapter = MainFragmentPagerAdapter(fragmentList, this@MainActivity)
// 3. 어댑터와 뷰 페이저 연결
viewPager.adapter = mainPagerAdapter
// 4. 탭 메뉴 구성요소 생성
val mainFragmentName = listOf(R.drawable.ic_baseline_chat_bubble_24,
R.drawable.ic_baseline_person_outline_24, R.drawable.ic_baseline_folder_open_24)
// 5. 탭 레이아웃과 뷰 페이저 연결
TabLayoutMediator(mainTab, viewPager){ tab, position->
tab.setIcon(mainFragmentName[position])
}.attach()
}
...
// ViewPager2를 위한 어댑터
class MainFragmentPagerAdapter(val fragmentList: List<Fragment>, fragmentActivity: FragmentActivity)
: FragmentStateAdapter(fragmentActivity){
override fun getItemCount() = fragmentList.size
override fun createFragment(position: Int) = fragmentList[position]
}
- ViewPager가 출력할 프래그먼트들을 담은 list를 생성한다.
- FragmentStateAdapter를 상속받은 클래스를 만들어 ViewPager2를 위한 어댑터를 생성한다. 이때 ViewPager가 출력할 프래그먼트들을 담은 list와 해당 ViewPager가 포함된 액티비티를 넘겨줘야 한다.
- 해당 어댑터를 ViewPager와 연결한다.
- TabLayout의 구성요소를 담은 list를 생성한다. 각 요소는 순서에 맞게 각 프래그먼트와 연결된다.
- TabLayout과 ViewPager를 TabLayoutMediator로 연결한다. 이때 TabLayout의 요소를 설정해준다.
3. 주의사항
- TabItem은 ID를 지정해주면 안된다!!
- 에러코드
java.lang.RuntimeException: Unable to start activity ComponentInfo{...}: java.lang.NullPointerException: Missing required view with ID: ...
- 발생경위
처음에 TabItem을 구분하고 뷰 바인딩으로 접근하기 위해 ID를 할당하였는데 Runtime에 NullPointerException이 발생했다.
- 원인
TabItem에 ID를 지정한 것이 문제였다. Material Design 라이브러리에서는 지원하지 않는 기능인 것 같다.
- 해결방법
TabItem의 ID를 제거하면 된다. TabItem에 대한 접근은 TabLayout.tab.position으로 index형태로 접근하면 된다.