코드스쿼드 미션 중 MainActivity에서 SubActivity에 있는 SettingFragment로 drawable 데이터를 전송해야 하는 상황이 있었는데 해당 내용을 구현하면서 고생을 많이 해서 기록으로 남겨 놓기로 했다.
📌 Tab 이란?
안드로이드에서 Tab은 서로 다른 컨텐츠를 서로 다른 화면에 보여줄 때 사용된다.
TabLayout
Tab은 Top AppBar 나 card, sheet, viewpager 등과 연결해서 사용할 수 있다.
그중은 안드로이드에서 탭호스트(TabHost)외에 탭(Tabs) 관련 기능을 구현할 때 사용할 수 있는 방법이다.
https://developer.android.com/reference/com/google/android/material/tabs/TabLayout
📌 ViewPager vs ViewPager2
ViewPager
좌우로 스와이프해 콘텐츠를 볼 수 있는 형태의 뷰
ViewPager2의 등장 이후 구글이 지원을 중단했다.
ViewPager2
여러 비효율성을 개발자들이 제기하였고 최신 버전인 2019년 ViewPager2 등장했다.
ViewPager2의 기능
- 세로 방향 지원
- ViewPager2는 세로 방향으로 슬라이드 하는 애니메이션을 지원한다.
- 오른쪽에서 왼쪽 지원 (Right to Left support)
- 우리나라는 상관없지만 다른 나라에는 오른쪽에서 왼쪽으로 읽는 문화도 있기 때문에 이런 나라에서는 왼쪽에서 오른쪽으로 증가하는 UI가 익숙하지 않고 어색하다고 느낄 수 있다. 따라서 이러한 불편함을 개선하여 오른쪽에서 왼쪽 지원 (Right to Left support)을 지원한다.
- ViewPager2에서 RecyclerView로 전환할 수 있고 RecyclerView 안에 ViewPager2를 넣어도 리소스를 많이 사용하지 않는다.
ViewPager2 종속성 추가
공식문서 대로 build.gradle 에 위처럼 종속성을 추가해준다.
📌 XML에서 TabLayout과 ViewPager 추가하기
TabLayout 추가
<?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=".SubActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:tabBackground="@drawable/tab_selector"
app:tabIconTint="@color/black"
app:tabIndicatorColor="@color/white"
app:tabIndicatorGravity="bottom"
app:tabIndicatorHeight="3dp"
app:tabTextColor="@color/black">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:icon="@drawable/ic_baseline_notifications_24"
tools:text="게임" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:icon="@drawable/ic_baseline_notifications_24"
tools:text="설정" />
</com.google.android.material.tabs.TabLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
위 코드에서
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:icon="@drawable/ic_baseline_notifications_24"
tools:text="게임" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:icon="@drawable/ic_baseline_notifications_24"
tools:text="설정" />
나는 코틀린 동적 코드에서 Tab의 Text 나 Icon을 설정해 주었기 때문에 이 부분은 안드로이드 스튜디오에서 Design을 볼 때 보기 편하게 하기 위해서 집어넣은 것이다 (nameSpace를 이용). 따라서 이 부분이 없어도 앱은 정상 작동한다.
ViewPager 추가
<?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=".SubActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager2"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/tabLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.viewpager2.widget.ViewPager2>
</androidx.constraintlayout.widget.ConstraintLayout>
전체 Layout 에서 TabLayout를 제외한 모든 부분에 ViewPager2 가 차지할 수 있도록 적절히 설정해준다.
나는 SettingFragment를 SubActivity의 ViewPager2 위치에 적용시킬 것이므로 우선 모두 SubActivity의 XML 파일에 설정해준다.
📌 SettingFragment의 XML 설정
<?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:id="@+id/constraintlayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.SettingFragment">
<ImageView
android:id="@+id/setting_fragment_imageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:contentDescription="@string/fragment_image_view"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
이제 이 SettingFragment를 SubActivity의 ViewPager2 위치에 놓을 것이다.
📌 SubActivity와 Fragment 연결
이제 SubActivity 와 Fragment 들을 연결시켜 주어야 한다. 나는 2개의 Tab에 2개의 Fragment를 연결시켜 줄 것이다.
이를 위해서 Adapter 클래스가 필요하다.
Adapter라는 클래스는 RecyclerView에서 제공하는 Adapter 클래스와 동일한 클래스이다. RecyclerView를 구현할 때 Adapter에 ViewHolder라는 것을 지정해주는 것처럼 FragmentStateAdapter가 지정되어 구현된다.
여기서 FragmentStateAdapter는 FragmentViewHolder를 ViewHolder로 가진 Adapter라는 클래스를 상속하고 있고 ViewPager2에 맞는 여러 메서드들을 추가로 구현해놓은 클래스이다.
RecyclerView와 ViewPager2는 같은 원리로 작동된다고 생각하면 된다.
FragmentStateAdapter는 필수적으로 2개의 메서드를 구현해야 한다.
getItemCount()
- FragmentStateAdapter의 public method는 아니다. FragmentStateAdapter가 상속하는 Recyclerview.Adapter 클래스의 public method이다.
- 해당 Adapter가 가지고 있는 data set 안에서의 전체 아이템 수를 리턴하는 메서드이다.
createFragment()
- FragmentStateAdapter의 public method이다.
- 특정 포지션에 연결된 새로운 Fragment를 제공하는 기능을 가지고 있는 메서드이다.
class SubActivity : AppCompatActivity() {
private lateinit var binding: ActivitySubBinding
@SuppressLint("UseCompatLoadingForDrawables")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySubBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val pagerAdapter = PagerAdapter(this)
binding.viewPager2.adapter = pagerAdapter
}
private inner class PagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> {
...
}
else -> {
...
}
}
}
}
나는 SubActivity 안에 inner class로 해당 부분을 구현했다. 또한 FragmentStateAdapter 클래스를 상속한 ViewPagerAdapter를 만들었으면 우리의 ViewPager2의 어댑터로 ViewPagerAdapter 클래스를 지정해 주어야 한다.
val pagerAdapter = PagerAdapter(this)
binding.viewPager2.adapter = pagerAdapter
TabLayout과 ViewPager 연결
// TabLayout 과 ViewPager 연결
TabLayoutMediator(binding.tabLayout, binding.viewPager2) { tab, position ->
when (position) {
0 -> {
tab.text = "게임"
tab.icon = tabIcons
}
1 -> {
tab.text = "설정"
tab.icon = tabIcons
}
}
}.attach()
TabLayoutMediator
A mediator to link a TabLayout with a ViewPager2. The mediator will synchronize the ViewPager2's position with the selected tab when a tab is selected, and the TabLayout's scroll position when the user drags the ViewPager2.
공식문서에서는 TabLayoutMediator 클래스를 Tab과 ViewPager를 synchronize 할 때 사용하는 클래스라고 소개한다.
첫 번째 인자인 tab을 이용해서 tab에 대한 작업을 처리해주고 when(position)을 통해서 몇 번째 tab에서 작업을 수행할 건지 지정해준다.
https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator
📌 Activity에서 Fragment로 drawable 전송
서론이 매우 길었다. 본격적으로 Activity에서 Fragment 로 drawable을 전송해보자.
나는 Activity에서 Activity 로 데이터를 전송할 때는 Intent 를 사용했고 Activity 에서 Fragment로 데이터를 전송할 때는 bundle을 이용하였다.
우선 MainActivity에서 버튼을 누르면 원하는 이모티콘(drawable 파일)의 id 값을 int로 bundle 에 담아서 이를 다시 intent 로 SubActivity 로 보내준다.
binding.button.setOnClickListener {
val intent = Intent(this, SubActivity::class.java)
val bundle = Bundle()
// drawable 파일은 기본적으로 int 타입으로 지정되어 있으므로 putInt 를 통해 bundle 에 넣는다.
bundle.putInt("firstBundle", R.drawable.ic_baseline_sentiment_satisfied_alt_24)
// 이를 SubActivity 로 보내주기 위해 다시 intent 에 넣는다.
intent.putExtra("firstIntent", bundle)
startActivity(intent)
}
이제 MainActivity 에서 SubAcitvity로 날아간 Intent를 처리해보자.
private inner class PagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> {
...
}
else -> {
val settingFragment = SettingFragment()
when {
intent.hasExtra("firstIntent") -> { // MainAcitvity 에서 보낸 키워드로 intent에서 bundle 을 꺼낸다.
val bundle = intent.getBundleExtra("firstIntent")
settingFragment.arguments = bundle
}
}
settingFragment
}
}
}
}
나는 두 번째 Tab에 SettingFragment를 지정할 것이므로 position 이 0이 아닌 else에서 원하는 작업을 처리해준다.
intent.hasExtra("firstIntent")
hasExtra를 통해 내가 찾는 인텐트가 MainActivity에서 왔는지 확인해준다.
val bundle = intent.getBundleExtra("firstIntent")
settingFragment.arguments = bundle
그리고 해당 인텐트를 번들로 꺼내 주고 그 번들을 다시 SettingFragment로 보내준다.
이제 SubActivity에서 보낸 번들을 SettingFragment에서 처리해주면 끝이다.
나는 지금 예시로 하나의 drawable 파일을 보내지만 SettingFragment 에서 여러 번들을 받아 그 키워드에 따라 나눠서 처리를 해야 할 수도 있다. 이때
val keySet = this.arguments?.keySet()
을 통해 키워드들을 Set <String> 타입으로 받아준다.
그리고 원하는 키워드를 찾아주고 SettingFragment의 imageView에 할당해 준다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val settingFragmentImageView = view.findViewById<ImageView>(R.id.setting_fragment_imageView)
val keySet = this.arguments?.keySet()
if (keySet != null) {
when {
"firstBundle" in keySet -> {
val drawable = this.arguments?.getInt("firstBundle")
if (drawable != null) {
settingFragmentImageView.setImageResource(drawable)
}
}
}
✌ 마치며
처음에 MainActivity에서 drawable 파일을 bundle로 감쌀 때 비트맵을 통해 데이터 자체를 보내려고 했다. 하지만 그 부분에 있어 지속해서 오류가 발생했고 디버깅을 통해 확인해본 결과 MainActivity -> SubActivity 까지는 정상적으로 데이터가 전달되지만 SubActivity -> SettingFragment로는 데이터가 전달되지 않는 것을 확인했다.
따라서 해결책을 모색하던 중 R.drawable. 이렇게 이름 지어진 int값만 넘겨줘서 해당 int값에 해당하는 drawable을 띄워주는 방법으로 진행하기로 했다. 비트맵이 아닌 int로 데이터를 보내니 정상적으로 작동하는 것을 확인할 수 있었다.
'Android' 카테고리의 다른 글
안드로이드 [Kotlin] - RecyclerView에서 ListAdapter와 DiffUtil 사용기 (0) | 2022.03.27 |
---|---|
안드로이드 [Kotlin] - LiveData로 계산기 만들기 (0) | 2022.03.27 |
안드로이드 [Kotlin] - RecyclerView로 랜덤한 색상으로 사각형 채워보기 (0) | 2022.03.25 |
안드로이드 [Kotlin] - TextView & Button (0) | 2022.02.20 |
안드로이드 [Kotlin] - 뷰 바인딩 (View Binding) (0) | 2022.02.20 |