Activitiy와 마찬가지로 사용자에게 UI 화면을 제공하는데 사용하는 컴포넌트
여러 개의 Fragment를 하나의 액티비티에 결합하여 다양한 화면 구성의 UI를제작할수 있음
Fragment는 항상 Activity 내에서 호스팅되어야함. 액티비티 위에 올라가는 화면의 일부, 즉 부분화면이라고 생각할 수 있음
태블릿과 같은 큰 화면에서 보다 역동적이고 유연한 UI디자인 지원하려는 목적으로 등장
액티비티의 생명주기에 직접적으로 영향을 받으면서 fragment만의 생명주기 역시 가짐
FragmentManager를 통해 Fragment 관리
Jetpack의 Navigation, BottomNavigationView, ViewPager2 등이 Fragment와 호환되도록 설계되어 있어 해당 라이브러리들과 함께 자주 사용됨
Activity 위에 Fragment A, B를 두고 동적으로 교체되도록 구현해보자.
blank Fragment를 안드로이드 스튜디오에서 하나 만들면 다음과 같이 생성된다.
class AFragment : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_a, container, false)
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment AFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
AFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
onCreateView
는 fragment의 레이아웃 구성을 담당하는 부분이다. 뷰 바인딩을 여기서 진행한다.
또한 ARG-PARAM
으로 변수를 받고 있다. onCreate에서 param들을 argument(setArgument)안에 넣고 있고,
newInstance가 companion object로 argument를 담은 변수들을 저장하는 자기 자신 fragment를 생성한다.
Fragment를 호출한다면 자바처럼 말한다면 new AFragment()와 같이 생성이 될 것이다. 하지만 새로 fragment를 만드는 것을 newInstance로 만드는 것을 안드로이드가 권장한다.AFragment.newInstance("paramA" , "paramB")
. 똑같이 new로 만드는 것인데 이 방법을 권장하는 이유는
단순히 new AFragment()와 같이 생성하고 setXxx로 param들을 넣어주면 framework에서 fragment가 제거되었다가 재생성될 때 유실된다. 하지만 newInstance 함수로 bundle로 argument를 전달해주면, 데이터 영속을 보장해준다.
onCreateView
UI 바인딩을 해주는 곳이다.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentABinding.inflate(inflater, container, false)
return binding.root
}
onViewCreated
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.statusTv.text = param1
binding.messageTv.text = param2
}
fragment의 params들로 UI를 초기화해준다.
onCreate() 이전에 onAttach(), onDestroy() 이후에 onDetach()로 액티비티에 연결, 연결 해제 과정도 포함된다.
Fragment를 생성하면서 넘겨준 값들이 있다면, 이 단계에서 값을 꺼내 세팅한다. UI 초기화는 onCreate에서 진행하지 않는다.
UI를 만들어준다. (즉 layout을 inflate 한다) View가 초기화되는 중이기 때문에 view에 값을 넣는 등 (binding.textview.text = 어쩌구) 작업을 하면 제대로 반영되지 않는다.
View 생성이 완료된 후 호출되는 메서드로
이 때 레이아웃 안의 뷰들을 초기화해줄 수 있다.
화면에 보이기 직전
사용자와의 상호작용 시작단계
프로그램이 다시 재개되면 초기화하는 작업이 필요한 경우 작업들을 세팅해줌
뷰바인딩을 한 경우 binding 된 객체를 다시 null로 초기화해줘야 한다. 프래그먼트는 뷰보다 오래 지속되기 때문이다.
Fragment를 생성하고 Activity에 부착해준 후, 여러 fragment간의 상호작용을 해주는 것이 중요한데,
fragment manager가 activity, fragment를 이어주는 역할을 한다. 프래그먼트의 추가, 교체, 삭제 작업에 대한 변경사항을 push 및 pop해준다.
https://developer.android.com/guide/fragments/fragmentmanager?hl=ko
activity에서 프래그먼트를 관리하는 fragment manager,
그안에 포함된 fragment들을 관리하는 fragment manager,
그안에 있는 child fragment를 관리하는 manager가 모두 있어 따로따로 access를 한다.
fragment transaction은 호스트 액티비티가 자신의 상태를 저장하기 전에 생성되고 커밋되어야 한다.
만약 호스트 액티비티가 onSaveInstanceState() 메소드를 호출한 후에 Fragment Transaction이 커밋된다면 에러가 발생한다.
fragment 전환 (replace)
binding.fragABtn.setOnClickListener {
//replace 인자 : fragment를 replace할 영역, 넘어가면서 생성할 fragment
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, AFragment.newInstance("ButtonClicked", getFormattedDate()))
.commit()
}
supportFragmentManager.beginTransaction()
에서 replace
메서드로 fragment가들어가는 activity의 view, fragment(new instance로 줘야 비정상적인 종료에도 params 전달이 보장)에 원하는 params를 넣어서 commit 해주면 된다.
참고로 replace가 아닌 add를 쓸 수도 있는데 이렇게 한다면 stack에 계속 쌓일 것이다.
fragment 제거 (remove)
binding.removeBtn.setOnClickListener {
//현재 화면에 있는 fragment 받아오기
val curFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
if (curFragment != null) {
supportFragmentManager.beginTransaction()
.remove(curFragment)
.commit()
}
else{
Toast.makeText(this, "제거불가", Toast.LENGTH_SHORT).show()
}
}
현재 화면에 있는 fragment를 받아오기 위해
val curFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
를 사용했다.
혹은 태그로 만들어서 그 태그 값을 사용해서 fragment를 가져올 수도 있다.
binding.addBtn.setOnClickListener {
childFragmentManager.beginTransaction()
.replace(R.id.frameLayout, CFragment(), "c_fragment")
.commitAllowingStateLoss()
}
binding.removeBtn.setOnClickListener {
val fragment = childFragmentManager.findFragmentByTag("c_fragment")
if (fragment != null) {
childFragmentManager.beginTransaction()
.remove(fragment)
.commitAllowingStateLoss()
}
}
위 예시에서는 Bfragment안에서 Cfragment를 부르고 있으므로 childFragmentManager
를 사용한다.
References
commit, commitAllowingStateLoss
https://medium.com/hongbeomi-dev/%EB%B2%88%EC%97%AD-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%A2%85%EB%A5%98%EC%9D%98-commit-8f646697559f
<FrameLayout
android:id="@+id/fragment_blank1"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include layout="@layout/fragment_blank1"/>
</FrameLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_blank2"
android:name="com.ssafy.fragment_2.howtoattach.BlankFragment2"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fragment_blank1" />
material에서 제공하는 tablayout을 삽입한다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity"
android:background="@android:color/darker_gray">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="월" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="화" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="수" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="목" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="금" />
</com.google.android.material.tabs.TabLayout>
<FrameLayout
android:id="@+id/frame_layout_main"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tab_layout_main" />
</androidx.constraintlayout.widget.ConstraintLayout>
binding.tabLayoutMain.addOnTabSelectedListener( object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
when (tab!!.position){
0 -> supportFragmentManager.beginTransaction()
.replace(R.id.frame_layout_main, TabItemFragment1())
.commit()
1 -> supportFragmentManager.beginTransaction()
.replace(R.id.frame_layout_main, TabItemFragment2())
.commit()
2 -> supportFragmentManager.beginTransaction()
.replace(R.id.frame_layout_main, TabItemFragment3())
.commit()
3 -> supportFragmentManager.beginTransaction()
.replace(R.id.frame_layout_main, TabItemFragment4())
.commit()
4 -> supportFragmentManager.beginTransaction()
.replace(R.id.frame_layout_main, TabItemFragment5())
.commit()
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
기존 xml에 framelayout 태그 대신 viewpagerlayout을 써서 fragment를 넣을 공간을 만들어준다. 또한 별도록 tablayout을 만들지 않고 코드 상에서 처리한다.
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tab_layout" />
TabLayoutPagerActivity.kt
class TabLayoutPagerActivity : AppCompatActivity() {
private lateinit var binding: ActivityTabPager2LayoutBinding
private val tabTitle = arrayOf("월","화","수", "목","금")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTabPager2LayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
val viewPager = binding.viewPager2
val tabLayout = binding.tabLayout
viewPager.adapter = ViewPagerAdapter(this)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = tabTitle[position]
}.attach()
}
class ViewPagerAdapter(activity: TabLayoutPagerActivity) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int {
return 5 //count..
}
override fun createFragment(position: Int): Fragment {
return when(position){
0 -> TabItemFragment1()
1 -> TabItemFragment2()
2 -> TabItemFragment3()
3 -> TabItemFragment4()
4 -> TabItemFragment5()
else -> TabItemFragment1()
}
}
}
override fun onBackPressed() {
if (binding.viewPager2.currentItem == 0) {
// If the user is currently looking at the first step, allow the system to handle the
// Back button. This calls finish() on this activity and pops the back stack.
super.onBackPressed()
} else {
// Otherwise, select the previous step.
binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1
}
}
}
onbackPressed
를 오버라이딩 하여 뒤로가기 버튼을 누를때 바로 activity를 끝내지 않고 특정 탭으로 이동할지를 지정해줄 수 있다.