Activity 생명주기
4대 컴포넌트 중에 하나인 Activity는 애플리케이션에서 보통 하나 이상의 Activity가 서로 연결된 형태로 구성어서 가장 기본이 되는 구성요소이다.
생명주기 ?
Life + cycle의 합성어, Lifecycle이다. 말 그대로 탄생하고 죽음에 이르기까지의 과정이라고 Activity에 적용한 것이다.
생명주기 호출 | |||
Activty 생성 시 | 화면에서 제거 | 종료 안하고 다른 Activity 실행 | |
유튜브 앱 시작 | 유튜브 실행 중 카톡 실행 | 카톡 완벽히 종료 | 유튜브 재 실행 |
youtube > onCreate() youtube > onStart() youtube > onResume() |
youtube > onPause() youtube > onStop() kakao > onCreate() kakao > onStart() kakao > onResume() |
kakao > onPause() kakao > onStop() kakao > onDestroy() youtube > onRestart() youtube > onStart() youtube > on Resume() |
youtube > onRestart() youtube > onStart() youtube > onResume() |
Activity는 실행부터 종료까지 많은 상태 변화를 거치며 상태가 변할 때마다 생명주기 함수가 자동으로 호출된다.
Activity의 상태는 ‘활성 상태’, ‘일시 정지 상태’, ‘비활성 상태’가 있다.
- Activity running(활성 상태)
- 사용자 화면에 보이고 있는 상태로 사용자 이벤트에 반응할 수 있는 상태이다.
- 생성된 Activity는 onCreate() → onStart() → onResume() 함수가 호출되면서 활성상태가 된다.
- 일반적으로 setContentView() 함수를 이용하여 액티비티 화면을 출력하는데, setContentView() 함수가 호출되는 시점이 화면 출력 순간이 아니라, onResume() 함수까지 실행하고 그 후 setContentView() 함수에서 출력한 내용이 화면에 나오는 구조이다. 따라서 onResume()이 함수가 실행되기까지 onCreate(), onStart(), onResume() 안에서 setContentView()를 호출하면 화면에 레이아웃이 출력됩니다.
- pause(일시 정지)
- - Activity가 화면에 보이지만 포커스를 잃은 상태이다.
- 예를 들어 dialog가 있다.
- onPause() 함수가 자동으로 호출된다.
- - Activity가 화면에 보이지만 포커스를 잃은 상태이다.
- stop(비활성 )
- 다른 Activity로 인해 화면이 완전히 가려진 상태이다.
- onPause() → onStop()
- 이후 다시 뒤로가기 등으로 돌아오면
- onRestart() → onStart() → onResume()
하나씩 설명
- onCreate()
- 가장 먼저 호출되는 메서드이다.
- 생명주기에서 단 한 번만 실행된다.
- 필수적으로 구현해야하고 Activity가 생성될 때 호출된다.
정리 : 시스템이 activity를 생성할 때 실행되고 Activity의 전체 생명주기 동안 한 번만 발생해야하는 메서드이다.
- onStart()
- Activity가 화면에 표시되기 직전에 호출된다.
- onCreate()를 호출한 뒤 실행되고 Activity가 사용자에게 표시되기 전에 호출된다.
정리 : Activity가 사용자에게 보여지기 직전에 필요한 초기화 작업을 수행한다.
- onResume()
- Activity가 화면에 보여지는 직후에 호출된다.
- 예를 들어 이벤트나 애니메이션을 시작할 수 있다.
정리 : Activity가 화면의 최상위에 위치하고 사용자와 상호 작용할 수 있는 상태가 될 때 호출된다.
- onPause()
- 다른 Activity가 화면 위로 나타나거나 일부 가려질 때 호출된다.
- 조심해야 할 점은 onPause()가 실행될 때는 아주 잠깐이므로 사용자 데이터 저장, 네트워크 호출, 데이터베이스 처리 등을 실행하면 안된다. 부하가 큰 종료 작업은 onStop() 상태일 때 실행되어야 한다.
- onStop()
- Activity가 다른 Activity에 의해 완전히 가려질 때 호출되는 메서드다.
- 예를 들어, 홈 키를 누르거나 다른 Activity로 이동할 경우가 있다.
정리 : Activity가 더 이상 사용자에게 표시되지 않을 때 호출된다. 완전히 가려서 보이지 않을 때 실행된다.
- onDestroy()
- Activity가 소멸되기 전에 호출된다. 사용자가 Activity를 닫거나 finish()가 호출되어 액티비트 종료될 때이다.
- 여기서는 Activity가 사용하는 리소스를 해제하거나 등의 마무리 작업을 수행할 수 있다.
- onRestart()
- onStop()이 호출 된 이후에 다시 기존 Activity로 돌아오는 경우 호출된다.
- onRestart()가 호출된 이후 이어서 onStart()가 호출된다.
Fragment 생명주기
Activity와 비슷한 콜백 메서드들도 있다.
Fragment에서 최소한으로 구현해야 하는 3개의 생명주기 메서드는
- onCreate()
fragment를 생성할 때 호출한다. fragment가 일시정지 혹은 중단 후 재개되었을 때 유지하고 있어야 하는 것을 여기서 초기화 해야한다.
- onCreateView()
fragment가 자신의 인터페이스를 처음 그리기 위해 호출한다. View를 반환해야 한다.
이 메서드는 fragment의 layout 루트이기 때문에 UI를 제공하지 않는 경우에는 null을 반환하면 된다.
- onPause()
사용자가 프래그먼트를 떠나면 첫번째로 이 메서드를 호출한다. 사용자가 돌아오지 않은 경우도 있으므로 여기에 현재 사용자 세션을 넘어 지속되어야 하는 변경사항을 저장한다.
하나씩 설명
- onAttach()
- fragment가 액티비티에 연결할 때 호출
- 연결 되기 전에 일부 초기화 작업을 수행할 수 있다.
- onCreate()
- 프래그먼트가 액티비티의 호출을 받아 생성한다.
- Bunddle로 액티비티로부터 데이터가 넘어온다.
- fragment의 초기화 작업을 수행할 수 있다.
+ onActivityCreated는 중단 됨
- onCreateView()
- 레이아웃 inflate 담당한다.
- savedInstanceState로 이전 상태에 대한 데이터 제공
- onViewCreated()
- onCreateView()를 통해 반환된 View 객체는 onViewCreated()의 파라미터로 전달 된다.
- 이 때 Lifecycle이 INITIALIZED 상태로 업데이트가 되었다.
- 때문에 View의 초기값 설정, LiveData 옵저빙, RecyclerView, ViewPager2에 사용될 Adapter 세팅은 이 메소드에서 해주는 것이 적절하다
- onViewStateRestored()
- 저장해둔 모든 state 값이 Fragment의 View의 계층 구조에 복원되었을 때 호출된다.
- ex) 체크박스 위젯이 현재 체크되어있는가
- View lifecycle owner : INITIALIZED → CREATED 변경
- onStart()
- 사용자에게 보여질 수 있을 때 호출된다.
- Activity의 onStart() 시점과 유사하다.
- Fragment의 childFragmentManager을 통해 FragmentTransaction을 안전하게 수행할 수 있음
- View lifecycle owner : CREATED → STARTED 변경
- onResume()
- 사용자와 프래그먼트가 상호작용 할 수 있는 상태일 때 호출
- Fragment가 보이는 상태에서 모든 Animator와 Transition 효과가 종료되고, 프래그먼트와 사용자가 상호작용 할 수 있을 때 onResume Callback이 호출된다.
- onPause()
- Fragment가 visible 일 때 onPause()가 호출된다.
- 이 때 Faragment와 View의 Lifecycle이 PAUSED가 아닌 STARTED가 됨
- onStop()
- Fragment가 더 이상 화면에 보여지지 않게 되면 onStop() 콜백 호출되게 된다.
- 부모 액티비티, 프래그먼트가 중단될 때, 상태가 저장될 때 호출
- View와 Lifecycle : STARTED → CREATED
- API 28버전을 기점으로 onSaveInstanceState() 함수와 onStop() 함수 호출 순서가 달라짐, 따라서 onStop()이 FragmentTransaction을 안전하게 수행하는 마지막 지점이 된다.
- onDestoryView()
- 모든 exit animation, transaction이 완료되고 Fragment가 화면으로부터 벗어났을 경우 호출된다.
- view와 lifecycle : CREATED → DESTROYED
- 가비지 컬렉터에 의해 수거될 수 있도록 Fragment View에 대한 모든 참조가 제거되어야 한다.
- getViewLifecycleOwnerLiveData()
- onDestroy()
- Fragment가 제거되거나, FragmentManager가 destroy 됐을 경우, onDestroy() 콜백 함수가 호출
- Fragment Lifecycle의 끝을 알림
- onDetach()
- 프래그먼트가 액티비티로부터 해제되어질 때 호출된다.
FirstFragment 생성 | FirstFragment -> SecondFragment | SecondFragment -> FirstFragment |
> onAttach > onCreate > onCreateView > onViewCreated > onViewStateRestored > onStart > onResume |
First> onPause First> onStop Second> onAttach Second> onCreate Second> onCreateView Second> onViewCreated Second> onViewStateRestored Second> onStart Second> onResume First> onDestroyView |
백버튼으로 돌아갈 경우 Second> onPause Second> onStop First> onCreateView First> onViewCreated First> onViewStateRestored First> onStart First> onResume Second> onDestroyView Second> onDestory Second> onDetach |
Activity Stack
- Activity 스택(Activity Stack)이라는 개념을 사용하여 화면 간 전환을 관리한다.
- Activity 스택은 앱이 실행 중일 때 각 화면(Activity)을 스택(Stack)에 쌓아두고, 이전 화면으로 돌아가거나 새로운 화면을 열 때마다 스택에 새로운 화면을 추가한다.
- 이를 통해 앱이 사용자의 탐색 흐름을 추적하고, 뒤로가기 버튼을 눌렀을 때 이전 화면으로 자연스럽게 돌아갈 수 있도록 한다.
단어 정리
Stack ?
스택은 후입선출(LIFO, Last-In-First-Out)의 구조를 가지며, 새로운 항목이 스택의 맨 위에 추가되고, 가장 최근에 추가된 항목이 가장 먼저 제거됩니다. 따라서 Activity 스택에서는 사용자가 현재 화면에서 뒤로가기 버튼을 누를 때마다 스택의 맨 위에서 가장 최근에 추가된 화면이 제거되고 이전 화면이 보여지는 것이다.
Task ?
Activity 스택은 Task라는 단위로 관리된다.
각 Task는 하나 이상의 Activity로 구성되어 있으며, 백스택(back stack)이라고도 불린다.
앱 아이콘을 눌러 앱을 다시 시작하면, 앱은 해당 Task의 최상위 Activity를 다시 보여준다.
back stack ?
각 Task는 백스택이라는 개념을 가지고 있다.
Activity나 Fragment가 스택(Stack) 자료 구조 형태로 관리되는 메모리 영역을 의미합니다.
사용자가 앱 내에서 다양한 화면을 이동할 때 사용되는 기록을 관리하며, 역순으로 사용자의 이전 동작을 추적한다.
stack을 관리하는 방법으로 크게 2가지가 있다.
1. LaunchMode
2. Intent Flag
1. AndroidManifest의 LaunchMode의 옵션으로 관리
launchMode ?
Activity의 특성을 정의하는데 사용되는 속성이다.
launchMode를 설정함으로써 Activity의 동작 방식을 지정할 수 있다.
1-1) android:launchMode="standard"
- 기본적인 런칭 모드로, 새로운 Activity 인스턴스가 항상 현재 Task의 백스택에 추가된다.
- 즉, Activity를 여러 번 시작할 때마다 각각 새로운 인스턴스가 생성되고 백스택에 추가되는 형식이다.
1-2) android:launchMode="singleTop"
- Activity를 시작할 때, 이미 백스택의 맨 위에 동일한 Activity 인스턴스가 있는 경우에는 새로운 인스턴스를 생성하지 않고 기존의 인스턴스를 재사용한다.
1-3) android:launchMode="singleTask"
- 새로운 Task에 시작되며, 해당 Task에는 단 하나의 인스턴스만 존재한다.
- 이미 Task에 존재하는 경우에는 해당 Task를 먼저 앞으로 이동시키고, 그 위에 새로운 인스턴스를 추가한다.
1-4) android:launchMode="singleInstance"
- 다른 launchMode와는 달리 새로운 Task에 시작된다.
- 따라서 해당 Activity를 시작하는 Task에는 해당 Activity 인스턴스만 포함된다.
- 또한 이 모드를 사용하는 Activity는 다른 Task와 분리되어 있으므로, 다른 Task에 속한 다른 Activity와 독립적으로 작동한다.
- 쉽게 말해, singleTask와 동일하지만 활동은 항상 자체 작업의 단 하나의 유일한 멤버로 위에 다른 Activity를 쌓을 수 없다.
2. Intent Flag로 관리
startActivity()에 전달하는 인텐트 플래그에 아래와 같은 플래그를 주어 스택을 관리한다.
2-1) FLAG_ACTIVITY_NEW_TASK
- singleTask와 유사하며 활동을 새 작업에서 시작한다.
- 이미 실행 중인 작업이 있다면 그 작업을 마지막으로 포그라운드로 이동하고 새 Intent를 수신한다
val intent = Intent(this, SecondActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
2-2) FLAG_ACTIVITY_CLEAR_TOP
- 호출하는 Activity 위에 있는 모든 Activity를 제거한다. 호출하는 Activity가 맨 위에 있지 않으면 새로운 인스턴스를 생성한다.
- 새롭게 부른 Activity가 가장 Top에 있는 Activity면 새 인스턴스가 생성되는 대신 기존 인스턴스가 호출을 수신한다.
- 예를 들어, 현재 애플리케이션의 모든 Activity를 제거하고 Main으로 다시 시작할 때 사용된다.
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
2-3) FLAG_ACTIVITY_SINGLE_TOP
- 호출하는 Activity가 이미 스택의 맨 위에 있는 경우 새로운 인스턴스를 시작하지 않고 기존 인스턴스를 재사용한다.
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
Fragment 데이터 전달 방식
1) bundle과 FragmentManager로 전달
전송하려는 Fragment
//PassBundleFragment는 본인이 전달하고자 하는 Fragment class
val bundle = Bundle()
bundle.putString("key", "value")
val passBundleBFragment = PassBundleBFragment()
passBundleBFragment.arguments = bundle parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container_bundle, PassBundleFragment())
.commit()
받을 Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
var result = arguments?.getString("key")
return inflater.inflate(R.layout.fragment_pass_b, container, false)
}
2) Fragment Result API를 이용하여 전달
FragmentManager은 fragment의 결과의 중앙 저장소 역할을 하기 때문에 fragment가 서로 직접 참조할 필요없이 fragment간 통신을 할 수 있다.
공식문서를 통해 실습을 해보았다.
데이터를 보내는 SenderFragment와 받는 ReceiverFragment를 만들었다.
위에 사진처럼 한 Activity위에 2개의 Fragment를 띄운다음
SenderFragment에는 2개의 버튼을 넣고 선택한 버튼에 따라 ReceiverFragment의 TextView값을 바꾸는 예제이다.
//ReceiverFragment
...
class ReceiverFragment : Fragment() {
private lateinit var binding:FragmentReceiverBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentReceiverBinding.inflate(inflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setFragmentResultListener("request") { key, bundle ->
bundle.getString("senderKey")?.let{
binding.receiveTextView.text = it
}
}
}
}
//SenderFragment
...
class SenderFragment : Fragment() {
private lateinit var binding:FragmentSenderBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentSenderBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding){
aButton.setOnClickListener {
// val bundle = Bundle()
// bundle.putString()
val bundle = bundleOf("senderKey" to binding.aButton.text)
setFragmentResult("request",bundle)
}
bButton.setOnClickListener {
val bundle = bundleOf("senderKey" to binding.bButton.text)
setFragmentResult("request",bundle)
}
}
}
}
setFragmentResult, setFragmentResultListener을 사용해서 key값으로 bundle의 값을 전달
3) Fragment간 shared ViewModel로 전달
ViewModel을 사용하여 데이터를 공유하는 방식
- ViewModel은 여러 fragmen간에 또는 fragment와 호스트 활동 간에 데이터를 공유해야 할 때 적합하다.
- Fragment가 Activity 하위에 위치하기 때문에 사용할 수 있다.
Activity를 공유하는 여러 Fragment들은 Activity의 메모리를 공유할 수 있고 AAC의 ViewModel은 Activity의 lifecycle보다 오래 살아있는 것이 보장되기 때문에 안전하게 공통의 Activity의 ViewModel을 사용하여 데이터 전달이 가능하다.
Activity에 Fragment들이 같은 데이터를 바라볼 수 있게 ViewModel 객체를 만들고 그 객체에 데이터를 전달하여 간단하게 Fragment 통신을 달성할 수 있다.
4) Jetpack의 Navigation에서 제공하는 safe-args로 전달
Jetpack의 Navigation을 사용하는 방식이다.
navigation에서 화면전환에 필요한 Action과 data를 정의할 수 있고, defaultValue도 정의가 가능하다.
간결하고 안정적인 처리의 장점이 있지만 다른 Data전달 방식보다 환경세팅하는 것이 많고 알아야 할 기능들이 많다.
직접 실습을 진행해보았다.
초기 설정
1. 2개의 Fragment생성, activity_main.xml 설정
(bottomnavigation은 개인적)
2. navigation 생성
데이터 이동
1. gradle
앱 수준의 gradle
plugins{
...
id("androidx.navigation.safeargs.kotlin")
}
프로젝트 수준의 gradle
plugins {
...
id("androidx.navigation.safeargs.kotlin") version "2.7.7" apply false
}
2. 초기에서 만든 navigation에서 action과 argument를 추가해준다.
<fragment
android:id="@+id/AFragment"
android:name="com.example.kotlin_navi_bottom.AFragment"
android:label="fragment_a"
tools:layout="@layout/fragment_a">
<action //추가
android:id="@+id/action_AFragment_to_BFragment"
app:destination="@id/BFragment" />
</fragment>
<fragment
android:id="@+id/BFragment"
android:name="com.example.kotlin_navi_bottom.BFragment"
android:label="fragment_b"
tools:layout="@layout/fragment_b">
<argument //추가
android:name="aData"
android:defaultValue="AA"
app:argType="string" />
</fragment>
GUI로도 할 수도 있다.
3. MianActivity.kt
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
binding.bottomNavigationView.setupWithNavController(navHostFragment.navController
nav_graph와 연결해주는 코드로 공식문서에 나와있다.
4. 데이터 처리
나는 AFragment의 데이터를 BFragment로 보낼 것이다.
그러면 AFragment의 fragmentDirection은 AFragmentDirections가 된다.
class AFragment : Fragment() {
private lateinit var binding: FragmentABinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentABinding.inflate(inflater, container, false)
binding.aButton.setOnClickListener {
val action = AFragmentDirections.actionAFragmentToBFragment(aData = "This is 'A'data")//
findNavController().navigate(action)
}
return binding.root
}
}
navigation의 argument name을 aData로 지정했었는데 그 aData의 arguments "This is 'A'data"를 전달해 준다.
class BFragment : Fragment() {
private lateinit var binding: FragmentBBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentBBinding.inflate(inflater, container, false)
val aData = BFragmentArgs.fromBundle(requireArguments()).aData
binding.bTextView.text = aData
return binding.root
}
}
결과 : A_Fragment의 버튼을 누르면 B_Fragment의 text가 argument에서 설정한 이름인 aData를 통해서 "This is 'A'Data"라는 Data로 바뀌게 된다.
전달 방식만 보려고 대충 만든 것이다.(내가 봐도 대충만든 것같아서...)
화면이 이동하는 이유는 action을 넣어줬기 때문이다.
ref
'Android Studio' 카테고리의 다른 글
Android - TabLayout 사용해서 Fragment 전환 (0) | 2024.03.26 |
---|---|
Android - SharedPreferences (0) | 2024.03.25 |
Android - Firebase Storage에 사진 올리기 (1/2) (1) | 2024.03.15 |
Android - FireStore 규칙 설정 / 데이터 가져오기 (1) | 2024.03.12 |
Kotlin - Firebase Auth로 간단한 로그인 구현 (1) | 2024.03.08 |