2024. 7. 16. 16:40ㆍ[개발]/Kotlin 활용 앱 개발
# 개념
- 액티비티 위에서 동작하는 모듈화된 UI
- 액티비티와 분리되어 독립적으로 동작할 수 없음.
- 여러 개의 프래그먼트를 하나의 액티비티에 조합하여 창이 여러 개인 UI를 구축 가능
- 하나의 프래그먼트를 여러 액티비티에서 재사용 가능
# 액티비티와 프래그먼트의 차이
1) 액티비티: 시스템의 액티비티 매니저에서 인텐트를 해석해 액티비티 간 데이터 전달
2) 프래그먼트: 액티비티의 프래그먼트 매니저에서 메소드로 프래그먼트 간 데이터 전달
# 프래그먼트 생명 주기
1) onAttach()
- 프래그먼트가 액티비티에 연결될 때 호출
- 아직 액티비티와 완전히 연결된 상태는 X
2) onCreate()
- 프래그먼트 생성 시 호출
- 초기화, 리소스 바인딩 등 수행
3) onCreateView()
- 프래그먼트의 레이아웃을 인플레이트
- 뷰를 생성하고 레이아웃을 설정
4) onActivityCreated()
- 액티비티의 onCreate() 가 완료된 후 호출
- 액티비티와 프래그먼트 뷰가 모두 생성된 상태이므로 뷰와 관련된 초기화 수행
5) onStart()
- 프래그먼트가 유저에게 보여줄 준비가 되었을 때 호출
- 필요한 리소스를 할당하거나, 애니메이션을 시작 가능
6) onResume()
- 프래그먼트가 유저와 상호작용할 수 있는 상태가 되었을 때 호출
- 프래그먼트가 포그라운드에 있을 때 실행되는 작업을 처리
7) onPause()
- 프래그먼트가 일시정지될 때 호출
- 상태 저장, 스레드 중지 등
8) onStop()
- 프래그먼트가 더 이상 사용자에게 보이지 않을 때 호출
- 리소스 해제, 스레드 정지 등
9) onDestroyView()
- 프래그먼트의 뷰와 관련된 리소스를 정리할 때 호출
10) onDestroy()
- 프래그먼트가 파괴될 때 호출
- 프래그먼트의 상태를 정리, 모든 리소스를 해제
11) onDetach()
- 프래그먼트가 액티비티로부터 분리될 때 호출
- 프래그먼트가 액티비티와의 모든 연결을 해제
# 프래그먼트 정의하기
0) Gradle에 implementation 추가하기
implementation("androidx.fragment:fragment-ktx:1.8.1")
1) 프래그먼트 레이아웃 생성
- 프래그먼트가 표시될 뷰들을 정의하는 xml 파일 생성(res/layout 안에)
2) 코틀린 소스 파일 생성
- 프래그먼트의 서브클래스 생성
- 프래그먼트에 대해 레이아웃을 제공하려면 반드시 onCreateVIew() 메서드 구현하기
- inflate() 함수를 통해 fragment_first.xml 파일에서 레이아웃을 로드하기
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_first, container, false)
}
3) 프래그먼트 선언하기
3-1) 액티비티의 레이아웃 파일 안에서 프래그먼트를 선언
- android:name = 레이아웃 안에서 인스턴스화할 프래그먼트 클래스 지정
- 각 프래그먼트에는 액티비티가 재시작될 때 프래그먼트를 복구하기 위해 시스템이 사용할 수 있는 고유 식별자가 필요함
<고유 식별자 제공하는 방법>
① android:id 속성 지정
② andriod:tag 속성 지정
③ id, tag 모두 없으면 시스템이 컨테이너뷰의 ID 사용
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<fragment
android:name="com.skmns.fragmentbasic.FirstFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragment" />
</LinearLayout>
3-2) kotlin 코드로 프래그먼트 추가하기
- supportFragmentManager: 사용자 상호작용에 응답해 프래그먼트를 추가하거나 삭제할 수 있게 해주는 매니저
- replace: 어느 프레임 레이아웃에 띄울 것인지, 어떤 프래그먼트인지 지정
- setReorderingAllowed: 애니메이션과 전환이 올바르게 작동하도록 트랜잭션과 관련된 프래그먼트의 상태 변경을 최적화
- addToBackStack: 뒤로가기 버튼 클릭 시 다음 액션(이전 프래그먼트로 이동 / 앱 종료)
# 프래그먼트의 데이터 전달
1) Activity to Fragment
// MainActivity(보내는 코드)
binding.run {
fragment1Btn.setOnClickListener{
// [1] Activity -> FirstFragment
val dataToSend = "Hello First Fragment! \n From Activity"
val fragment = FirstFragment.newInstance(dataToSend)
setFragment(fragment)
}
fragment2Btn.setOnClickListener {
// [1] Activity -> SecondFragment
val dataToSend = "Hello Second Fragment!\n From Activity"
val fragment = SecondFragment.newInstance(dataToSend)
setFragment(fragment)
}
- 프래그먼트의 인스턴스를 생성(fragment1Btn, fragment2Btn)
- newInstance 메소드를 통해 데이터를 Fragment로 전달
- setFragment()로 fragment를 설정(set)
// FirstFragment(받는 코드)
private var param1: String? = null
companion object {
@JvmStatic
fun newInstance(param1: String) =
// [1] Activity -> FirstFragment
FirstFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// [1] Activity -> FirstFragment
binding.tvFrag1Text.text = param1
}
- companion object 블록 내의 메서드, 변수는 클래스의 정적 멤버처럼 사용될 수 있음
(정적 멤버: 클래스의 인스턴스가 아닌, 클래스 자체에 속하는 멤버(변수, 메서드 등) / 반대: 인스턴스)
- @JvmStatic: 이 메서드가 정적 메서드처럼 호출될 수 있게 함
- newInstance() : 프래그먼트를 생성하고 초기화하는 데 사용 / param1이라는 String 값을 매개변수로 받아 ARG_PARAM1을 key값으로 하여 Bundel() 객체에 담고, 이 Bundle() 객체를 프래그먼트의 인자(arguments) 로 설정
( FirstFragment의 arguments(인자)는 String 값인 Bundle(ARG_PARAM1, param1) )
- FirstFragment.apply() 를 통해 새 프래그먼트 객체를 생성하고 초기화함 (newInstance())
- arguments에 Bundle() 객체를 설정해 프래그먼트에 데이터를 전달
- onViewCreated(): 프래그먼트 뷰가 생성된 후 호출되는 메서드
- super.onViewCreated(): 상위 클래스의 onViewCreated() 메서드를 호출해 기본 구현을 수행
- binding.tvFrag1Text.text = param1: binding 객체를 통해 프래그먼트의 textView에 param1(Bundle)의 값을 설정
*** 실습할 때 오류났음!
코드
// 수정 전 (1) - onCreateView 메서드
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_second, container, false)
}
// 수정 후 (1) - onCreateView 메서드
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentSecondBinding.inflate(inflater, container, false)
return binding!!.root
}
오류 난 이유
- 질문하고 추가!
// 수정 전 (2) - onViewCreated 메서드
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.secondFragmentText.text = param1
}
}
// 수정 후 (2) - onViewCreated 메서드
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.secondFragmentText?.text = param1
}
}
2) Fragment to Fragment
// FirstFragment.kt(보내는 코드)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.firstFragmentText?.text = param1
binding.btnGofrag2.setOnClickListener{
val dataToSend = "Hello Fragment2! \n From Fragment1"
val fragment2 = SecondFragment.newInstance(dataToSend)
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.frameLayout, fragment2)
.addToBackStack(null)
.commit()
}
- onViewCreated() 메서드 안에 데이터 전송하는 코드 작
- SecondFragment의 newInstance 메서드를 호출해 데이터를 인자로 전달하면서 두 번째 프래그먼트 인스턴스를 생성
- requireActivity(): 현재 프래그먼트가 속한 액티비티를 반환
- supportFragmentManager(): 프래그먼트 매니저를 호출
- beginTransaction: 새로운 프래그먼트 트랜잭션 => 프래그먼트 전환
- replace(): 지정된 컨테이너 뷰(R.id.frameLayout)에 현재 프래그먼트를 새 프래그먼트로 교체
- addToBackStack(): 현재 트랜잭션을 백스택에 추가해 뒤로가기 버튼을 누르면 현재 프래그먼트로 돌아오도록 설정
(인자로 null 설정하면 기본 이름으로 백스택에 추가됨)
- commit(): 트랜잭션을 적용해 실제로 프래그먼트 교체
private const val ARG_PARAM1 = "param1"
class SecondFragment : Fragment() {
private var param1: String? = null
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// [2] Fragment -> Fragment
binding.tvFrag2Text.text = param1
}
companion object {
@JvmStatic
fun newInstance(param1: String) =
// [1] Activity -> FirstFragment
SecondFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
// Binding 객체 해제
_binding = null
}
}
- ARG_PARAM1: 프래그먼트에 데이터를 전달하기 위한 키
- _binding: 프래그먼트의 뷰 바인딩 객체를 저장하는 변수
- binding: _binding에 접근할 수 비공개 접근자 / null이 아님을 보장
- onCreate(): 프래그먼트가 생성될 때 호출 / arguments가 존재하면 ARG_PARAM1 키를 사용해 데이터를 빼와서 params 변수에 저장
- onCreateView(): 프래그먼트의 뷰 계층을 생성하고 반환
- FragmentSecondBinding.inflate: 뷰 바인딩 객체를 생성
- onViewCreated(): 프래그먼트의 뷰가 생성된 후 호출
- binding.tvFrag2Text.text = param1: TextView에 param1의 값을 설정
- companion object: 클래스의 정적 멤버를 포함
- newInstance(): SecondFragment의 새 인스턴스를 생성, param1 값을 Bundle에 담아 인자로 전달
- onDestroyView(): 프래그먼트의 뷰가 파괴될 때 호출
- _binding을 null로 설정해 메모리 누수를 방지
3) Fragment to Activity
// SecondAvtivity: 보내는 코드
private const val ARG_PARAM1 = "param1"
// 프래그먼트와 액티비티 간 데이터 전송을 위한 콜백 인터페이스 생성
interface FragmentDataListener {
// 프래그먼트에 데이터를 액티비티에 전송할 때 호출하는 메서드
fun onDataReceived(data: String)
}
class SecondFragment : Fragment() {
// FragmentDataListener 인터페이스 객체 / 나중에 액티비티와 연결
private var listener: FragmentDataListener? = null
private var param1: String? = null
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
// 프래그먼트가 액티비티에 연결될 때 호출되는 메서드
override fun onAttach(context: Context) {
super.onAttach(context)
// context가 FragmentDataListener인 경우 listener 변수에 context를 할당해
// 프래그먼트가 액티비티에 데이터를 전송할 수 있도록 함
if (context is FragmentDataListener) {
listener = context
} else {
throw RuntimeException("$context must implement FragmentDataListener")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// [2] Fragment -> Fragment
binding.tvFrag2Text.text = param1
// [3] SecondFragment -> Activity
binding.btnSendActivity.setOnClickListener{
val dataToSend = "Hello from SecondFragment!"
listener?.onDataReceived(dataToSend)
}
}
companion object {
@JvmStatic
fun newInstance(param1: String) =
// [1] Activity -> FirstFragment
SecondFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
// Binding 객체 해제
_binding = null
listener = null
}
}
// MainActivity: 받는 코드
// FragmentDataListener 코드를 구현하는 MainActivity 클래스를 생성
class MainActivity : AppCompatActivity(), FragmentDataListener {
// ActivityMainBinding 객체를 지연 초기화하는 binding 객체 생성
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
binding.apply {
fragment1Btn.setOnClickListener{
// [1] Activity -> FirstFragment
val dataToSend = "Hello First Fragment! \n From Activity"
val fragment = FirstFragment.newInstance(dataToSend)
setFragment(fragment)
}
fragment2Btn.setOnClickListener {
// [1] Activity -> SecondFragment
val dataToSend = "Hello Second Fragment!\n From Activity"
val fragment = SecondFragment.newInstance(dataToSend)
setFragment(fragment)
}
}
setFragment(FirstFragment())
}
private fun setFragment(frag : Fragment) {
supportFragmentManager.commit {
replace(R.id.frameLayout, frag)
setReorderingAllowed(true)
addToBackStack("")
}
}
// [3] SecondFragment -> Activity
override fun onDataReceived(data: String) {
// Fragment에서 받은 데이터를 처리
Toast.makeText(this, data, Toast.LENGTH_SHORT).show()
}
}
'[개발] > Kotlin 활용 앱 개발' 카테고리의 다른 글
Android - RecyclerView에서의 ListAdapter 활용 (0) | 2024.07.18 |
---|---|
Android - RecyclerView에서 클릭 이벤트를 전달하기 (1) | 2024.07.18 |
Android - 알림(Notification) (0) | 2024.07.17 |
Android - 다이얼로그(Dialog) (0) | 2024.07.17 |
Android - ListView, GridView (2) | 2024.07.16 |