깨알 개념/Android

[Android] Fragment의 생명주기

interfacer_han 2024. 1. 24. 12:28

#1 이전 글

[Android] Activity의 생명주기

#1 Android Application의 시작 (AndroidManifext.xml) ... 우리가 앱 아이콘을 누르면, 안드로이드 시스템은 해당 앱의 Launcher Activity를 실행시킨다. AndroidManifext.xml의 등록된 액티비티 중에서 태그를 가지고

kenel.tistory.com

프래그먼트는 액티비티와 비슷하지만, 가장 주요한 차이점은 홀로 설 수 없으며 반드시 액티비티나 또 다른 프래그먼트에 종속되어 동작한다는 것이다. 프래그먼트는 액티비티처럼 생명주기를 가지며, 종속된 액티비티나 또 다른 프래그먼트의 생명주기에 영향을 받기도 한다. 이전 글에 이어 이번에는 Fragment의 생명주기를 살펴본다. 
 

#2 Fragment의 생명주기 흐름도

각 생명주기에 해당하는 콜백 함수들의 역할은 Activity에서와 거의 비슷하다. 설명보다는 실제 앱을 통해 감을 잡는 것이 좋다. 이전 글에서와 마찬가지로, 생명주기에 따른 콜백 함수들에 Log를 달아 생명주기를 살펴볼 수 있는 앱을 만들어보았다.
 

#3 프래그먼트 생명주기 살펴보기용 앱

#3-1 개요

액티비티 2개(MainActivity, SecondActivity)를 사용한다. 각각의 액티비티는 바텀 내비게이션 바를 가지고 있다. 바텀 내비게이션 바의 아이템이 3개씩이므로, 프래그먼트도 3개씩 가지고 있다. 프래그먼트 내부의 버튼을 눌러 액티비티 간 이동도 가능하다.
 

#3-2 살펴보기용 앱을 만들기 위한 템플릿

[File] → [New] → [New Project...] → [Bottom Navigation Views Activity]의 템플릿으로 프로젝트를 만들고, 수정했다.
 

#3-3 MainActivity.kt

// package com.example.fragmentlifecyclelogger

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import com.example.fragmentlifecyclelogger.databinding.ActivityMainBinding
import com.google.android.material.bottomnavigation.BottomNavigationView

class MainActivity : AppCompatActivity(), OnBottomNavUiChangeListener {

    private lateinit var binding: ActivityMainBinding
    private lateinit var navView: BottomNavigationView
    private lateinit var navController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {
        Log.i("interfacer_han", "MainActivity.onCreate()")
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        navView = binding.navView
        navView.setOnItemSelectedListener { item ->
            Log.i("interfacer_han", "(MainActivity) ${item.title} clicked")
            navController.navigate(item.itemId)
            true
        }

        navController = findNavController(R.id.nav_host_fragment_activity_main)
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        val appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
            )
        )
        setupActionBarWithNavController(navController, appBarConfiguration)
        //navView.setupWithNavController(navController) // 위에 있는 커스텀 리스너로 대체

        navigateFragment()
    }

    override fun onStart() {
        Log.i("interfacer_han", "MainActivity.onStart()")
        super.onStart()
    }

    override fun onNewIntent(newIntent: Intent?) {
        super.onNewIntent(newIntent)
        intent = newIntent
    }

    private fun navigateFragment() {
        Log.i(
            "interfacer_han",
            "(MainActivity) intent.getStringExtra(\"fragment_info_key\"): ${intent.getStringExtra("fragment_info_key")}"
        )

        /* 위에 있는 onNewIntent()가 있어야,
         * 스택에 있다가 재호출(onResume())된 Activity의 intent.getStringExtra("fragment_info_key")의 값이
         * 하나로 고정되지 않는다.
         */
        when (intent.getStringExtra("fragment_info_key")) {
            null -> {/* 아무 것도 안함. 기본값으로 설정된 프래그먼트 호출됨 */}

            "HomeFragment" -> {
                navController.navigate(R.id.navigation_home)
            }

            "DashboardFragment" -> {
                navController.navigate(R.id.navigation_dashboard)
            }

            "NotificationsFragment" -> {
                navController.navigate(R.id.navigation_notifications)
            }
        }
    }

    override fun onResume() {
        Log.i("interfacer_han", "MainActivity.onResume()")
        super.onResume()

        navigateFragment()
    }

    override fun onPause() {
        Log.i("interfacer_han", "MainActivity.onPause()")
        super.onPause()
    }

    override fun onStop() {
        Log.i("interfacer_han", "MainActivity.onStop()")
        super.onStop()
    }

    override fun onRestart() {
        Log.i("interfacer_han", "MainActivity.onRestart()")
        super.onRestart()
    }

    override fun onDestroy() {
        Log.i("interfacer_han", "MainActivity.onDestroy()")
        super.onDestroy()
    }

    override fun changeBottomNavUi(selectedItemId: Int) {
        val menuItem = navView.menu.findItem(selectedItemId)
        if(!menuItem.isChecked) {
            menuItem.isChecked = true
        }
    }
}

SecondActivity의 코드도 거의 같다. 전체 소스코드는 #5에 있다.
 

#3-4 HomeFragment.kt

// package com.example.fragmentlifecyclelogger.ui.home

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.fragment.app.Fragment
import com.example.fragmentlifecyclelogger.OnBottomNavUiChangeListener
import com.example.fragmentlifecyclelogger.R
import com.example.fragmentlifecyclelogger.SecondActivity
import com.example.fragmentlifecyclelogger.databinding.FragmentHomeBinding

class HomeFragment : Fragment() {

    private var _binding: FragmentHomeBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onCreateView()")
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        val root: View = binding.root

        val button: Button = binding.buttonHome
        button.setOnClickListener {
            val intent = Intent(activity, SecondActivity::class.java)
            intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) // 이 플래그가 없으면, Activity를 전환할 때마다 Activity가 onCreate()됨
            intent.putExtra("fragment_info_key", "Home2Fragment")
            startActivity(intent)
        }
        return root
    }

    override fun onDestroyView() {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onDestoryView()")
        super.onDestroyView()
        _binding = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onCreate()")
        super.onCreate(savedInstanceState)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onViewCreated()")
        super.onViewCreated(view, savedInstanceState)
    }

    override fun onViewStateRestored(savedInstanceState: Bundle?) {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onViewStateRestored()")
        super.onViewStateRestored(savedInstanceState)
    }

    override fun onStart() {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onStart()")
        super.onStart()

        val nowActivity = activity
        if(nowActivity is OnBottomNavUiChangeListener) {
            nowActivity.changeBottomNavUi(R.id.navigation_home)
        }
    }

    override fun onResume() {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onResume()")
        super.onResume()
    }

    override fun onPause() {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onPause()")
        super.onPause()
    }

    override fun onStop() {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onStop()")
        super.onStop()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onSaveInstanceState()")
        super.onSaveInstanceState(outState)
    }

    override fun onDestroy() {
        Log.i("interfacer_han", "(MainActivity) HomeFragment.onDestroy()")
        super.onDestroy()
    }
}

나머지 5개의 프래그먼트들의 코드도 거의 같다. 전체 소스코드는 #5에 있다.
 

#4 로그 확인

액티비티의 생명주기는 빨간색 글씨로 표시했다.
 

더보기

1. 시작 화면 (MainActivity)
    MainActivity.onCreate()
    HomeFragment.onCreate()
    HomeFragment.onCreateView()
    HomeFragment.onViewCreated()
    MainActivity.onStart()
    HomeFragment.onViewStateRestored()
    HomeFragment.onStart()
    MainActivity.onResume()
    HomeFragment.onResume()
    
2. MainActivity 내에서 Dashboard 프래그먼트로 전환
    HomeFragment.onPause()
    HomeFragment.onStop()
    DashboardFragment.onCreate()
    DashboardFragment.onCreateView()
    DashboardFragment.onViewCreated()
    DashboardFragment.onViewStateRestored()
    DashboardFragment.onStart()
    HomeFragment.onDestoryView()
    DashboardFragment.onResume()
    
3. SecondActivity의 Dashboard2 프래그먼트로 이동
    MainActivity.onPause()
    DashboardFragment.onPause()
    SecondActivity.onCreate()
    Home2Fragment.onCreate()
    Home2Fragment.onCreateView()
    Home2Fragment.onViewCreated()
    SecondActivity.onStart()
    Home2Fragment.onViewStateRestored()
    Dashboard2Fragment.onCreate()
    Dashboard2Fragment.onCreateView()
    Dashboard2Fragment.onViewCreated()
    Dashboard2Fragment.onViewStateRestored()
    Home2Fragment.onDestoryView()
    Dashboard2Fragment.onStart()
    SecondActivity.onResume()
    Dashboard2Fragment.onStop()
    Dashboard2Fragment.onCreate()
    Dashboard2Fragment.onCreateView()
    Dashboard2Fragment.onViewCreated()
    Dashboard2Fragment.onViewStateRestored()
    Dashboard2Fragment.onStart()
    Dashboard2Fragment.onDestoryView()
    Dashboard2Fragment.onResume()
    MainActivity.onStop()
    DashboardFragment.onStop()
    HomeFragment.onSaveInstanceState()
    DashboardFragment.onSaveInstanceState()
    
4. SecondActivity 내에서 Notification2 프래그먼트로 전환
    Dashboard2Fragment.onPause()
    Dashboard2Fragment.onStop()
    Notifications2Fragment.onCreate()
    Notifications2Fragment.onCreateView()
    Notifications2Fragment.onViewCreated()
    Notifications2Fragment.onViewStateRestored()
    Notifications2Fragment.onStart()
    Dashboard2Fragment.onDestoryView()
    Notifications2Fragment.onResume()
    
5. MainActivity의 Notification 프래그먼트로 이동
    SecondActivity.onPause()
    Notifications2Fragment.onPause()
    MainActivity.onRestart()
    MainActivity.onStart()
    DashboardFragment.onStart()
    MainActivity.onResume()
    DashboardFragment.onStop()
    NotificationsFragment.onCreate()
    NotificationsFragment.onCreateView()
    NotificationsFragment.onViewCreated()
    NotificationsFragment.onViewStateRestored()
    NotificationsFragment.onStart()
    DashboardFragment.onDestoryView()
    NotificationsFragment.onResume()
    SecondActivity.onStop()
    Notifications2Fragment.onStop()
    Dashboard2Fragment.onSaveInstanceState()
    Notifications2Fragment.onSaveInstanceState()
    Home2Fragment.onSaveInstanceState()
    Dashboard2Fragment.onSaveInstanceState()

6. MainActivity 내에서 Dashboard 프래그먼트로 전환
    NotificationsFragment.onPause()
    NotificationsFragment.onStop()
    DashboardFragment.onCreate()
    DashboardFragment.onCreateView()
    DashboardFragment.onViewCreated()
    DashboardFragment.onViewStateRestored()
    DashboardFragment.onStart()
    NotificationsFragment.onDestoryView()
    DashboardFragment.onResume()

 

#5 요약

프래그먼트는 작은 Activity처럼 동작한다.
 

#6 완성된 앱

https://github.com/Kanmanemone/android-practice/tree/master/lifecycle/FragmentLifecycleLogger