Fragment에 대해 알아보자 | Android Study
🙄 Fragment?
액티비티처럼 이용할 수 있는 View
액티비티처럼 이용할 수 있는 View
기존에는 휴대폰 화면이 작았기 때문에 Activity
만을 이용하여 화면을 구성해도 아무런 문제가 없었다. 그러나, 태블릿 PC의 등장으로 화면이 넓어지며 Activity
하나로 UI, 사용자 이벤트를 처리하기엔 너무 복잡해지고 코드가 길어졌다.
그래서 처음으로 나온 대안이 View
클래스로 Activity
의 역할을 대신하는 것이었지만, 오로지 화면 출력만을 위해 설계된 클래스이기 때문에 Activity
의 생명주기를 이용할 수 없어 모든 역할을 대신할 수 없었다.
View
클래스 중 액티비티와 생명주기가 같은 클래스가 등장한다면 Activity
의 코드 중 일부를 분리해서 개발할 수 있게될 것이고, 이런 목적으로 등장한 클래스가 바로 Fragment
이다.
한 줄로 요약하면, Activity
에서 처리하는 임무들을 Fragment
가 대신 일부를 처리해주는 것이다.
특징
-
Fragment
는 독립적일 수 없다.
:Activity
혹은 부모Fragment
에 종속적이다. -
Fragment
는 자체적으로 생명주기를 가진다. -
Fragment
는 재사용이 가능하다.
:Fragment
는 여러Activity
에서 생성 및 사용할 수 있다. -
Activity
내에서 실행 중 추가, 삭제, 교체 등이 가능하다.
자세히 알아보기 전, Fragment
의 기본 클래스 두 개를 조금만 알아보도록 하자.
FragmentManager
FragmentManager
는 Activity
혹은 Fragment
에서 휘하의 Fragment
를 관리하는 클래스로, 각각 하나씩만 가지고 있다. 이 클래스를 통해 Activity
-Fragment
혹은 부모 Fragment
- 자식 Fragment
는 서로 상호작용을 할 수 있게 된다.
FragmentTransaction
FragmentTransaction
에서 실질적으로 Fragment
를 추가, 삭제, 교체 등 여러 작업을 진행한다. 뿐만 아니라, Fragment
의 백스택 관리, Fragment
전환 시 애니메이션 설정 역시 FragmentTransaction
을 이용해 진행한다.
🙋🏼♂️ 백스택? : 사용자가 뒤로가기 버튼을 눌렀을 때 Activity 처럼 이전 Fragment 화면이 나오게 설정하는 것을 의미한다.
참고로, 사용하는 메소드는 아래와 같다.
-
add()
: 새로운Fragment
를 컨테이너에 추가한다. -
replace()
: 기존 컨테이너에 있는Fragment
를 대체한다. -
remove()
: 컨테이너에 있는Fragment
를 제거한다. -
show()
: 컨테이너에 있는Fragment
를 보여준다.(visibility = true)
-
hide()
: 컨테이너에 있는Fragment
를 숨긴다.(visibility = false)
-
commit()
: 작업을 실행한다. -
commitNow()
: 백스택이 없을 경우에만 사용하며, 작업을 즉시 수행한다.
🤔 Fragment LifeCycle
기본적으로 Fragment
는 Activity
위에서 생성되기 때문에, Activity
의 생명주기와 함께 봐야 할 필요가 있다.
또, Fragment
는 Activity
와 달리 Fragment View
의 생명주기도 가지고 있다. 두 개의 생명주기를 사진을 보며 잘 파악해보자.
onAttach()
Fragment
가Activity
에 포함되는 순간 호출된다. 즉,Activity
에 종속되는 과정.
onCreate()
Fragment
가 생성됐을 때 호출되지만,Fragment View
는 포함되지 않은 상태이다. 이 곳에서View
관련 세팅을 진행하면 안정성을 보장받지 못한다.
-
onCreateView()
Fragment
의 UI 구성을 위해 호출되며Fragment View
의 생명주기가 생성된다. -
onViewCreated()
onCreateView()
를 통해View
객체를 전달받는다. 이 단계에서View
의 초기 세팅을 하면 안정성을 보장받을 수 있다.
onResume()
Fragment
와 사용자가 상호작용이 가능한 상태이다.
-
onDestroyView()
Fragment
가 화면에서 사라진 후,Fragment View
의 생명주기를 없앤다. 이 시점에서Fragment View
에 대한 모든 참조를 제거해야 가비지 컬렉터가Fragment View
를 수거해갈 수 있다. 만약 백스택 처리를 했다면onDestroy()
로 가지 않고 이 단계에서 머무른다. -
onDetach()
Fragment
가Activity
에서 완전히 제거될 때 호출된다.
🙃 실습
📌 XML 설정
<?xml version="1.0" encoding="utf-8"?>
<!-- MainActivity -->
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.SampleFragment.AppBarOverlay">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="?actionBarSize"
android:padding="16dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="first"/>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="second"/>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="third"/>
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- MainActivity -->
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.SampleFragment.AppBarOverlay">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="?actionBarSize"
android:padding="16dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="first"/>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="second"/>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="third"/>
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"/>
</LinearLayout>
Activity
의 XML에 TabLayout
을 추가해 탭을 클릭할 때마다 Fragment
를 실행하도록 할 예정이다.
<?xml version="1.0" encoding="utf-8"?>
<!-- Fragment -->
<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=".MainActivity">
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello First Fragment"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment
의 XML인데, TextView
를 그냥 가운데에 둔 것이다. XML을 잘 설정했다면, 아래와 같은 화면이 나올 것이다.
📌 MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
supportFragmentManager.beginTransaction()
.replace(R.id.container, FirstFragment.newInstance(5))
.commit()
binding.tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
when (tab?.text.toString()) {
"first" -> supportFragmentManager.beginTransaction()
.replace(R.id.container, FirstFragment.newInstance(5))
.commit()
"second" -> supportFragmentManager.beginTransaction()
.replace(R.id.container, SecondFragment.newInstance(5))
.commit()
else -> supportFragmentManager.beginTransaction()
.replace(R.id.container, ThirdFragment.newInstance(5))
.commit()
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
// NOT IMPLEMENTS
}
override fun onTabReselected(tab: TabLayout.Tab?) {
// NOT IMPLEMENTS
}
})
}
}
위에서 이야기했던 FragmentManager
와 FragmentTransaction
을 이용하여 Fragment
를 화면에 나타나게 하는 코드이다.
supportFragmentManager.beginTransaction()
.replace(R.id.container, FirstFragment.newInstance(5))
.commit()
supportFragmentManager
를 이용해 Activity
의 FragmentManager
를 가져오는 것과 beginTransaction()
을 이용해 기존에 설정한 컨테이너에 Fragment
화면을 담는 모습을 확인할 수 있다.
여기서 드는 의문점인데, 저렇게 일일이 beginTransaction
을 작업 실행 때마다 코드에 넣을 필요가 있을까? 이런 식으로 하면 안되는걸까?
binding.tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
val transaction = supportFragmentManager.beginTransaction()
override fun onTabSelected(tab: TabLayout.Tab?) {
when (tab?.text.toString()) {
"first" -> transaction
.replace(R.id.container, FirstFragment.newInstance(5))
.commit()
"second" -> transaction
.replace(R.id.container, SecondFragment.newInstance(5))
.commit()
else -> transaction
.replace(R.id.container, ThirdFragment.newInstance(5))
.commit()
}
}
아쉽지만 화면을 담는 작업을 마치고 commit()
을 완료한 FragmentTransaction
객체에 다시 commit()
명령을 시도하면 오류가 발생한다. 하나의 Transaction
에는 하나의 commit()
, 반드시 기억하도록 하자.
그렇다면, newInstance()
코드는 무엇일까?
📌 Fragment
class FirstFragment : Fragment() {
private var _binding : FragmentFirstBinding? = null
private val binding get() = _binding!!
private var num : Int? = null
companion object {
fun newInstance(count : Int): FirstFragment{
val args = Bundle()
args.putInt("number", count)
val fragment = FirstFragment()
fragment.arguments = args
return fragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(arguments != null) {
num = requireArguments().getInt("number")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.count.text = num.toString()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Activity
와 마찬가지로 Fragment
도 재생성 되는 경우가 많다. 이렇게 재생성되는 과정에서 Activity
혹은 Fragment
에게 전달받은 데이터를 보존하기 위해, newInstance()
메소드에서 스스로를 생성한다.
테스트를 위해 밑에 count를 넣었고, 화면을 돌려보았으나 데이터는 그대로 유지되는 것을 확인할 수 있었다.
만약 보존할 데이터가 없다면 굳이 newInstance()
를 사용할 필요는 없다.
override fun onTabSelected(tab: TabLayout.Tab?) {
when (tab?.text.toString()) {
"first" -> supportFragmentManager.beginTransaction()
.replace(R.id.container, FirstFragment())
.commit()
"second" -> supportFragmentManager.beginTransaction()
.replace(R.id.container, SecondFragment())
.commit()
else -> supportFragmentManager.beginTransaction()
.replace(R.id.container, ThirdFragment())
.commit()
}
}
위와 같이 코드를 설정한다면, Fragment
가 기본 생성자를 알아서 호출하여 화면을 띄워준다.
그리고 _binding = null
부분이 이해가 안간다면 해당 포스팅을 참고하면 된다. 짧게 요약하면 메모리 누수를 방지하기 위해 Fragment View
에 대한 참조를 제거하여 가비지 컬렉터가 수거해가도록 하는 것이다.
📌 Fragment 간 데이터 공유
데이터를 공유하는 방법에는 여러가지 방법이 존재한다.
Bundle
-FragmentManager
로 전달하는 방법Fragment Result API
를 사용하여 전달하는 방법Fragment
간 공통의ViewModel
로 전달하는 방법
그 중에서 ViewModel
을 이용해 데이터를 공유하는 방법에 대해 알아본다. ViewModel
을 다룬 포스팅에서도 살짝 다룬 적이 있는데, 한번 코드를 넣어가며 자세히 보도록 하자.
우선 XML에 Button
을 추가한다.
ViewModel
class MainViewModel : ViewModel() {
private val _count = MutableLiveData<Int>()
val count : LiveData<Int> get() = _count
init {
_count.value = 5
}
fun getUpdatedCount(plusCount: Int){
_count.value = (_count.value)?.plus(plusCount)
}
}
ViewModel
을 위와 같이 간단하게 작성한다.
Fragment
class FirstFragment : Fragment() {
private var _binding : FragmentFirstBinding? = null
private val binding get() = _binding!!
private val TAG = "FirstFragment"
private val viewModelFactory = ViewModelProvider.NewInstanceFactory()
private val viewModel: MainViewModel by lazy {
ViewModelProvider(requireActivity(), viewModelFactory)[MainViewModel::class.java] // 데이터 공유
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate()")
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(TAG, "onViewCreated()")
viewModel.count.observe(viewLifecycleOwner) {
binding.count.text = it.toString()
Log.d(TAG, "Observing. . . . . . ")
}
binding.plus.setOnClickListener {
viewModel.getUpdatedCount(1)
}
}
}
버튼을 클릭했을 때 ViewModel
의 count
변수의 크기가 1 증가하는 코드로 구성하였다. 가장 키포인트로 봐야할 것은 아래의 코드이다.
private val viewModel: MainViewModel by lazy {
ViewModelProvider(requireActivity(), viewModelFactory)[MainViewModel::class.java] // 데이터 공유
}
이 부분에서, ViewModelStoreOwner
, 즉 ViewModelStore
의 주인으로 부모 Activity
를 넘겨준다.
만약, 여기서 requireActivity()
가 아니라 this
로 매개변수를 넘겨주었다면, Fragment
간 데이터 공유는 할 수 없고 해당 Fragment
에서만 ViewModel
의 데이터가 저장되는 결과가 나올 것이다.
세 번째 Fragment
에는 this
로 매개변수를 넘겨주니 데이터 공유가 안되는 것을 확인할 수 있다.
📌 Fragment With LiveData
LiveData 를 Fragment
와 같이 사용할 때 생기는 문제점이 있다. 아래의 코드를 보자.
viewModel.count.observe(viewLifecycleOwner) {
binding.count.text = it.toString()
Log.d(TAG, "Observing. . . . . . ")
}
여기서, viewLifeCycleOwner
는 무슨 뜻일까? 위에서 이야기했던, Fragment View
의 생명주기를 뜻한다.
그렇다면 왜 Fragment
의 생명주기가 아닌 Fragment View
의 생명주기를 넘겨주어야 하는지 알아보도록 하자.
Fragment
가 재개되는 과정을 보면 Activity
와 달리 onDestroy()
메소드가 실행되지 않는 것을 확인할 수 있다.
그 과정에서 onCreateView()
가 여러번 실행될 수 있는데, 그렇게 되면 LiveData
에 있는 기존 Observer
는 사라지지 않고, 새로운 Observer
가 등록되어 여러번 Observer
가 호출되는 현상이 발생한다.
그렇기 때문인지 실제 안드로이드 스튜디오에서도 매개변수를 this
, 즉 Fragment
의 생명주기를 넘겨주려 하면 빨간줄로 경고를 표시한다. 이를 무시하고 실행하면 아래와 같은 결과가 나온다.
Fragment
에서 생명주기를 관리한다면 안드로이드 스튜디오에서 친절하게 경고해주니 부담이 덜 가지만, 만약 BaseFragment
에서 lifecycleOwner
를 this
로 설정한다면 오류가 발생할 수 있으니, 꼭 숙지해두도록 하자.
Fragment
를 쓰면서도 왜 쓰는지에 대해서 생각이 많이 부족했던 것 같아 포스팅을 하며 여러 내용을 정리하였다.
해당 포스팅을 보신 여러분께도 많은 도움이 되었길 바랍니다........... 🥲
연습하며 썼던 코드도 링크 걸어두니 사용하실 분들은 사용하시면 됩니다 :)
참고 및 출처
The Android Lifecycle cheat sheet — part III : Fragments
Fragment 1 — Fragment의 이해와 생성
안드로이드 공식 문서
깡쌤의 안드로이드 프로그래밍
Fragment Lifecycle과 LiveData
Author And Source
이 문제에 관하여(Fragment에 대해 알아보자 | Android Study), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@hoyaho/Fragment저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)