[본 캠프(앱 개발 숙련) 강의내용 정리]
1. Fragment
- 정의 : Acitivity 위에서 동작하는 모듈화된 사용자 인터페이스
- 특징 : 액티비티와 분리되어 독립적으로 동작할 수 없음
-> Fragment 위의 Host가 Fragment가 될 수는 있지만, 최상단 Host에는 무조건 Activity가 위치해야함
- 사용시기 : 아래와 같이 TabLayout을 구현한다고 할 때, 각 탭의 UI를 Fragment로 구현할 수 있음.
- 화면 하나당 Activity가 매칭되게 구현하지 말 것
- (의문점) 그렇다면, 각 탭의 UI를 Activity가 아닌 Fragment로 구현했을 때 어떤 장점이 있길래 써야하는 걸까?
-> 장점 1. 높은 성능 : Activity로 화면을 계속 전환하는 것보다 Fragment로 일부만 바꾸는 게 자원 이용량이 적어 속도가 빠르기 때문 (Fragment만 바꿔치기 해도 여러 UI를 보여줄 수 있기 때문에, Activity를 전환하는 시간 및 자원을 절약할 수 있다.)
-> 장점 2. 유연성 : 런타임 시 UI 디자인을 실시간으로 수정할 수 있음 (역동적이고 유연한 UI 구현이 가능함)
-> 장점 3. 적응성 : 여러 Fragment를 준비해뒀다가 다양한 해상도(Phone, Tablet)의 사이즈에 맞게 띄울 Fragment를 지정할 수 있음
-> 장점 4. 모듈화(재사용성, 유지보수성) : 특정 기능을 구현하는 Fragment를 만들어서 다양한 액티비티에서 재사용할 수 있다 / 하나만 고쳐도 사용한 모든 곳에서 반영된다.
2. Fragment 사용법
MainActivity에서 xml에 FrameLayout과 버튼 2개를 정의하고 각각 버튼을 클릭했을 때, Fragment 1 또는 2를 띄우는 예제
- Fragment Base Code
// SecondFragment도 코드 동일함
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.fragment_blank, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 데이터 연동
}
}
만약 viewBinding을 쓴다면 아래와 같음
class FirstFragment : Fragment() {
private val binding by lazy {
FragmentFirstBinding.inflate(layoutInflater)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 데이터 연동
}
}
- Activity Code
class MainActivity : AppCompatActivity() {
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
val buttons = listOf(binding.btnFirst, binding.btnSecond)
buttons.forEach { button ->
button.setOnClickListener {
button.connectFragment()
}
}
}
private fun Button.connectFragment() = with(binding) {
when(this@connectFragment) {
btnFirst -> setFragment(FirstFragment())
btnSecond -> setFragment(SecondFragment())
}
}
private fun setFragment(frag : Fragment) {
// SFM : 사용자의 상호작용에 응답해 Fragment를 add 또는 remove 할 수 있는 class
supportFragmentManager.commit {
// fl_main 레이아웃에 frag라는 Fragment를 추가하겠다.
replace(R.id.fl_main, frag)
// 애니메이션과 전환이 올바르게 작동하도록 트랜잭션과 관련된 프래그먼트의 상태 변경을 최적화
setReorderingAllowed(true)
// 뒤로가기 버튼 클릭 시, 다음 액션 지정
addToBackStack("") // -> 다음 액션 없음
}
}
}
3. Fragment를 Activity(_main).xml 파일에 추가하는 방법 2가지
(1) 정적 할당, Activity(_main).xml에서 <fragment>를 추가한 뒤 사용한 Fragment의 경로를 name에 지정한다.
<LinearLayout
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/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<fragment
android:id="@+id/fm_first"
android:name="com.example.fragmenttest.FirstFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
(2) 동적 할당, Kotlin Code 상에서 SFM을 활용하여 Activity(_main).xml에 프래그먼트를 추가한다.
private fun setFragment(frag : Fragment) {
// SFM : 사용자의 상호작용에 응답해 Fragment를 add 또는 remove 할 수 있는 class
supportFragmentManager.commit {
// fl_main 레이아웃에 frag라는 Fragment를 추가하겠다.
replace(R.id.fl_main, frag)
// 애니메이션과 전환이 올바르게 작동하도록 트랜잭션과 관련된 프래그먼트의 상태 변경을 최적화
setReorderingAllowed(true)
// 뒤로가기 버튼 클릭 시, 다음 액션 지정
addToBackStack("") // 종료?
}
}
4. Fragment의 데이터 전달방식 3가지 (스탠다드 2주차 강의 내용)
액티비티-프래그먼트 or 프래그먼트-프래그먼트의 경우 아래와 같은 방법이 있다.
4-1. Bundle : Bundle 객체 생성 -> 데이터 셋업 -> setArguments()
- 2. Fragment Result Api : 위에서 언급했던 FM을 활용
- A: setFragmentsResult("key", bundle) -> FM -> B: setFragmentsResultListener("key")
- 3. ViewModel : ViewModel을 사용하여 데이터 공유 -> 공부해보자
5. Fragment의 데이터 전달 흐름 3가지 (A -> F, F -> A, F -> F)
다음은 Activity와 fragment 간의 데이터 전달 흐름에 맞게 코딩한 내용을 기술한다.
위의 4. 데이터 전달 방식에서 4-1. Bundle 객체를 활용하는 방법이다.
5-1. (Main)Activity -> (First)Fragment
private const val ARG_KEY_PARAM1 = "param1" // [1] Activity -> FirstFragment
class FirstFragment : Fragment() {
private lateinit var param1: String // [1] Activity -> FirstFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 2. 아래 Factor 메서드에서 받아온 arguments 값을 param1에 저장
arguments?.let {
param1 = it.getString(ARG_KEY_PARAM1, "null")
}
}
// 데이터 연동
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 3. 위에서 백업한 param1값 사용
binding.tvFrag.text = param1
}
companion object {
// 1. Factor 메서드, 액티비티에서 값을 받아온 뒤 arguments에 저장.
fun newInstanceWithParam(data: String): FirstFragment {
return FirstFragment().apply {
arguments = Bundle().apply {
putString(ARG_KEY_PARAM1, data)
}
}
}
}
}
Main Activity 호출 코드의 변화
// 데이터 전달 전
private fun Button.connectFragment() = with(binding) {
when(this@connectFragment) {
btnFirst -> {
val frag = FirstFragment()
setFragment(frag)
}
...
}
}
// 데이터 전달 후
private fun Button.connectFragment() = with(binding) {
when(this@connectFragment) {
btnFirst -> {
val dataToSend = "Data Flow \n\n (Main)Activity -> (First)Fragment"
// 위에서는 바로 객체를 생성했지만, 지금은 데이터 전달을 위해 Factor(data) 메서드 호출
val frag = FirstFragment.newInstanceWithParam(dataToSend)
setFragment(frag)
}
...
}
}
5-2. (First)Fragment -> (Second)Fragment
- 5-2-1. 위와 같은 방식으로 Bundle을 활용함
private const val ARG_KEY_PARAM2 = "param2"
class SecondFragment : Fragment() {
private lateinit var param2: String
private val binding by lazy {
FragmentSecondBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 2. 아래 Factor 메서드에서 받아온 arguments 값을 param2에 저장
arguments?.let {
param2 = it.getString(ARG_KEY_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return binding.root
}
// 데이터 연동
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 3. 위에서 백업한 param2값 사용
binding.tvFrag.text = param2
}
}
companion object {
// 1. Factor 메서드, Second Fragment에서 값을 받아온 뒤 arguments에 저장.
fun newInstanceWithParam(data: String): SecondFragment {
return SecondFragment().apply {
arguments = Bundle().apply {
putString(ARG_KEY_PARAM2, data)
}
}
}
}
}
First Fragment 호출 코드
class FirstFragment : Fragment() {
...
// 데이터 연동
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
// [1] (Main)Activity -> (First)Fragment
...
// [2] (First)Fragment -> (Second)Fragment
btnFrag.setOnClickListener {
val dataToSend = "Data Flow \n\n (First)Fragment -> (Second)Fragment"
val secondFragment = SecondFragment.newInstanceWithParam(dataToSend)
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.fl_main, secondFragment)
.addToBackStack(null)
.commit()
}
}
}
...
}
5-3. (Second)Fragment -> (Main)Activity
- 이부분 부터는 인터페이스와 상속, 오버라이드 개념을 사용한다.
(1) 데이터 전달을 위한 인터페이스 구현
interface FragmentDataListener {
fun onDataReceived(data: String)
}
(2) Main Activity에서 인터페이스 상속 및 구현
class MainActivity : AppCompatActivity(), FragmentDataListener {
...
override fun onDataReceived(data: String) {
Toast.makeText(this, data, Toast.LENGTH_SHORT).show()
}
}
(3) SecondFragment에서 listener 객체를 초기화하고, 이 객체를 이용해서 MainActivity의 onDataReceived() 메서드 호출
class SecondFragment : Fragment() {
private lateinit var listener: FragmentDataListener
override fun onAttach(context: Context) {
super.onAttach(context)
// 1. mainActivity가 FragmentDataListener를 구현하고 있는지 체크
if (context is FragmentDataListener) {
listener = context // true면 listener 셋업
} else {
throw RuntimeException("$context must implement FragmentDataListener")
}
}
// 데이터 연동
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnFrag.setOnClickListener {
val dataToSend = "(Second)Fragment -> (Main)Activity"
// 2. 셋업된 listener를 활용하여 MainActivity의 onDataReceived 함수 실행
listener?.onDataReceived(dataToSend)
}
}
...
}
- 풀코드 (아래 접은글)
class MainActivity : AppCompatActivity(), FragmentDataListener {
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
val buttons = listOf(binding.btnFirst, binding.btnSecond)
buttons.forEach { button ->
button.setOnClickListener {
button.connectFragment()
}
}
}
private fun Button.connectFragment() = with(binding) {
when(this@connectFragment) {
btnFirst -> {
val dataToSend = "Data Flow \n\n (Main)Activity -> (First)Fragment"
val frag = FirstFragment.newInstanceWithParam(dataToSend)
setFragment(frag)
}
btnSecond -> {
val dataToSend = "Data Flow \n\n (Main)Activity -> (Second)Fragment"
val frag = SecondFragment.newInstanceWithParam(dataToSend)
setFragment(frag)
}
}
}
private fun setFragment(frag : Fragment) {
// SFM : 사용자의 상호작용에 응답해 Fragment를 add 또는 remove 할 수 있는 class
supportFragmentManager.commit {
// fl_main 레이아웃에 frag라는 Fragment를 추가하겠다.
replace(R.id.fl_main, frag)
// 애니메이션과 전환이 올바르게 작동하도록 트랜잭션과 관련된 프래그먼트의 상태 변경을 최적화
setReorderingAllowed(true)
// 뒤로가기 버튼 클릭 시, 다음 액션 지정
addToBackStack("") // 종료?
}
}
// [3] (Second)Fragment -> (Main)Activity
override fun onDataReceived(data: String) {
Toast.makeText(this, data, Toast.LENGTH_SHORT).show()
}
}
private const val ARG_KEY_PARAM1 = "param1" // [1] Activity -> FirstFragment
class FirstFragment : Fragment() {
private lateinit var param1: String // [1] Activity -> FirstFragment
private val binding by lazy {
FragmentFirstBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// [1] Activity -> FirstFragment
arguments?.let {
param1 = it.getString(ARG_KEY_PARAM1, "null")
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return binding.root
}
/**
* 데이터 연동
*/
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
// [1] (Main)Activity -> (First)Fragment
tvFrag.text = param1
btnFrag.setOnClickListener {
// [2] (First)Fragment -> (Second)Fragment
val dataToSend = "Data Flow \n\n (First)Fragment -> (Second)Fragment"
val secondFragment = SecondFragment.newInstanceWithParam(dataToSend)
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.fl_main, secondFragment)
.addToBackStack(null)
.commit()
}
}
}
companion object {
fun newInstanceWithParam(data: String): FirstFragment {
return FirstFragment().apply {
arguments = Bundle().apply {
putString(ARG_KEY_PARAM1, data)
}
}
}
}
}
private const val ARG_KEY_PARAM2 = "param2"
class SecondFragment : Fragment() {
// [3] (Second)Fragment -> (Main)Activity
private lateinit var listener: FragmentDataListener
private var param2: String? = null
private val binding by lazy {
FragmentSecondBinding.inflate(layoutInflater)
}
override fun onAttach(context: Context) {
super.onAttach(context)
// [3] (Second)Fragment -> (Main)Activity
if (context is FragmentDataListener) {
listener = context
} else {
throw RuntimeException("$context must implement FragmentDataListener")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param2 = it.getString(ARG_KEY_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return binding.root
}
// 데이터 연동
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
// [2] (First)Fragment -> (Second)Fragment, FirstFrag로 부터 받아온 데이터 출력
tvFrag.text = param2
// [3] (Second)Fragment -> (Main)Activity
btnFrag.setOnClickListener {
val dataToSend = "(Second)Fragment -> (Main)Activity"
listener?.onDataReceived(dataToSend)
}
}
}
companion object {
fun newInstanceWithParam(data: String): SecondFragment {
return SecondFragment().apply {
arguments = Bundle().apply {
putString(ARG_KEY_PARAM2, data)
}
}
}
}
}
5-2-2. Fragment -> Fragment 좀 더 간단한 코드 (동일하게 Bundle 사용 - 스탠다드 3주차 강의 내용)
- 보내는 Fragment
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
btnSend.setOnClickListener {
// 0. 데이터 준비
val dataSource = DataSource.getDataSource()
val data: List<Flower> = dataSource.getFlowerList()
// 1. 데이터 셋업을 위한 Bundle 객체 생성
val bundle = Bundle().apply {
putParcelable("flowerData", data[0]) // class type 전달
}
// 2. arguments에 데이터를 셋업한 bundle 지정
val frag = DashBoardFragment().apply {
arguments = bundle
}
parentFragmentManager.beginTransaction()
.replace(R.id.fl_main, frag)
.commit()
}
}
}
}
- 받는 Fragment
private const val KEY = "flowerData"
class DashBoardFragment : Fragment() {
private val binding: FragmentDashboardBinding by lazy {
FragmentDashboardBinding.inflate(layoutInflater)
}
// 0. 데이터 저장할 변수 미리 정의
private lateinit var receivedData: Flower
override fun onAttach(context: Context) {
super.onAttach(context)
// 1. 데이터 받기
arguments.let {
if (it!!.containsKey(KEY)) {
receivedData = it.getParcelable<Flower>(KEY)!!
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 2. 데이터 쓰기
binding.tvText1.text = "이름: ${receivedData.name} \n설명: ${receivedData.description}"
}
}
추가로 학습 해야할 내용!
- viewModel 적용 시, Activity-Fragment간의 데이터 전달방법 정리
'Android' 카테고리의 다른 글
앱 개발 숙련 과제 후기 (1) | 2024.04.18 |
---|---|
Retrofit2 (2) | 2024.04.15 |
RecyclerView의 Adapter 구현 및 Item Click Event 처리방법 정리 (0) | 2024.04.11 |
Adapter, AdapterView 및 ListView, GridView 정리 (0) | 2024.04.11 |
스탠다드 2주차 강의내용 정리 및 과제(LifeCycle) (0) | 2024.04.10 |