Android - Fragment

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()
    }
}