이번 내용에서는 카운트를 측정하는 앱을 만들텐데, 간단한 앱이지만 중요한 개념인 생명주기에 대한
데이터 유지 방법에 대해 설명할 것입니다.
설명을 보다 쉽게 하기 위해서 코드부터 보여주며 설명하겠습니다.
[activity_main.xml]
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainActivity">
<TextView
android:id="@+id/txt_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:text="초기화"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txt_count" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="더하기"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_reset"
app:layout_constraintTop_toBottomOf="@+id/txt_count" />
</androidx.constraintlayout.widget.ConstraintLayout>
다음 xml 형태로 셋팅하게 되면 아래와 같이 UI가 구성됩니다.
초기화 버튼을 클릭하면 0으로 초기화되고 더하기 버튼을 클릭하면 하나씩 숫자가 커지는 앱입니다.
정말 간단한 앱이지만 여기서 핵심은 생명주기에 따른 데이터 유지 형태입니다.
[MainActivity.kt]
class MainActivity : ComponentActivity() {
private var number: Int = 0
companion object {
const val TAG = "MainActivity"
const val STATE_CODE = "1001"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("onCreate()!!!!!!!!")
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val txtCount = findViewById<TextView>(R.id.txt_count)
val btnPlus = findViewById<AppCompatButton>(R.id.btn_plus)
val btnReset = findViewById<AppCompatButton>(R.id.btn_reset)
if(savedInstanceState != null){
txtCount.text = savedInstanceState.getInt(STATE_CODE).toString()
}
btnPlus.setOnClickListener{ _ ->
number += 1
txtCount.text = number.toString()
}
btnReset.setOnClickListener{ _ ->
number = 0
txtCount.text = number.toString()
}
}
override fun onStart() {
println("onStart()!!!!!")
super.onStart()
}
override fun onResume() {
println("onResume()!!!!!")
super.onResume()
}
// OnStart() 메소드 다음에 호출되는 녀석
// 시스템은 복원할 저장 상태가 있을 경우에만 호출(Bundle에 값을 저장해 놓을 경우에만 이게 호출된다는 것)
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
println("onRestoreInstanceState!!!!!!!")
number = savedInstanceState.getInt(STATE_CODE)
super.onRestoreInstanceState(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
println("haams_number : ${number}")
outState?.run {
putInt(STATE_CODE, number)
}
super.onSaveInstanceState(outState)
}
}
기본으로 생기는 부분을 제외하고 코드에 대해서만 설명하자면, onCreate() 함수안에서 txt_count, btn_plus, btn_reset에 대한
id 값을 찾고 이걸 객체로 만든 다음에 setOnClickListener를 두는데, 람다 형식으로 두어 txtCount의 값이 하나씩 더해지거나
초기화되도록 구현해뒀습니다.
이 부분은 간단하겠지만 여기서 핵심은 onCreate()에서의 이 부분입니다.
if(savedInstanceState != null){
txtCount.text = savedInstanceState.getInt(STATE_CODE).toString()
}
savedInstanceState를 Bundle로 두고 있는데 이 값이 존재한다면, 코드값으로 저장한 정보를 가지고와서
txtCount에 값으로 넣는 것입니다. 또한 이 부분도 중요합니다.
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
println("onRestoreInstanceState!!!!!!!")
number = savedInstanceState.getInt(STATE_CODE)
super.onRestoreInstanceState(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
println("haams_number : ${number}")
outState?.run {
putInt(STATE_CODE, number)
}
super.onSaveInstanceState(outState)
}
onSaveInstanceState라는 오버라이딩된 함수는 SaveInstanceState라는 Bundle 객체에 값을 추가하여 넣는겁니다.
여기선 outState라는 Bundle에 확장함수 run을 이용해서 number라는 값을 넣어줍니다.
그리고 이 InstanceState가 다시 복구될 때 해당 Bundle 값에서 getInt로 (당시 키 값을 그대로 넣어서 가져옴)
number를 가져옵니다.
이렇게 하게 되면 카운팅 앱에서 더하기 버튼을 클릭해서 숫자를 늘린 다음에 화면 전환을 할 경우
화면전환, 즉 Configuration이 변경된다면 앱이 종료된 것으로 체크하여 생성을 다시합니다.
따라서 먼저 앱이 일시정지하고 멈춘 다음에 현재 상태 값을 먼저 저장하고자 onSaveInstanceState 함수를 호출한 다음
앱이 종료됩니다.
그리고 다시 생성하고 그 과정에서 이미 저장된 값을 가지고 와서 화면에 뿌려줍니다.
이후 시작 주기가 거친 다음 현 상태를 복기하는 onRestoreInstanceState가 호출됩니다.
그리고 앱이 재개됩니다.
화면 전환이 이루어진 다음부터 순서는 아래와 같습니다.
2024-07-18 23:55:29.808 10694-10694 System.out com.example.countnumberapp I Pause!!!!!
2024-07-18 23:55:29.808 10694-10694 System.out com.example.countnumberapp I stop!!!!
2024-07-18 23:55:29.808 10694-10694 System.out com.example.countnumberapp I haams_number : 5
2024-07-18 23:55:29.808 10694-10694 System.out com.example.countnumberapp I onDestroy!!!!
2024-07-18 23:55:29.823 10694-10694 System.out com.example.countnumberapp I onCreate()!!!!!!!!
2024-07-18 23:55:29.840 10694-10694 System.out com.example.countnumberapp I onStart()!!!!!
2024-07-18 23:55:29.840 10694-10694 System.out com.example.countnumberapp I onRestoreInstanceState!!!!!!!
2024-07-18 23:55:29.840 10694-10694 System.out com.example.countnumberapp I onResume()!!!!!
haams_number로 찍힌 것이 onSaveInstanceState 함수이며, 여기서 Bundle에다가 앱이 죽기전 기존 값 number를 넣어둡니다.
화면 전환이 마친 다음에 앱이 시작되었을때, onCreate()가 호출됩니다.
if(savedInstanceState != null){
txtCount.text = savedInstanceState.getInt(STATE_CODE).toString()
}
이 때, 이 로직이 실행되면서 앞서 저장한 Bundle에 데이터를 넣어줍니다.
그 결과 화면이 전환되었음에도 UI 내 데이터는 변경되지 않습니다.
그리고 onRestoreInstanceState가 진행될 때 여기서도 number라는 값을 유지하고자 Bundle에서 값을 가져옵니다.
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
println("onRestoreInstanceState!!!!!!!")
number = savedInstanceState.getInt(STATE_CODE)
super.onRestoreInstanceState(savedInstanceState)
}
이유는 현재 전환된 화면에서도 보여지는 것 뿐만 아니라 실제 number라는 값 또한 유지가 되어야 하므로
이렇게 number라는 값에 셋팅해주면 전환된 화면에서도 더하기 버튼을 누를때 문제없이 잘 작동할 것입니다.
마지막으로 내용 정리를 하자면, 구성변경시 (가로/세로 모드 변경) onPause, onStop 후 onSaveInstanceState가 호출되고
onDestroy가 됩니다.
다음 onCreate, onStart가 수행되고 onRestoreInstanceState가 진행된 뒤 onResume이 수행됩니다.
이 생명주기에 맞춰서 기존 데이터를 유지한 상태로 (임시 UI 상태 저장 및 복원) 앱이 작동하도록 구현해야 합니다.
이러한 시스템 제약으로 인해 활동이 소멸할 경우 데이터를 유지하기 위해서는 다음 세 가지 방법을 이용하여 현 UI 상태를
임시 저장하고 생명주기에 맞춰 복원해 서비스합니다.
1) ViewModel
2) onSaveInstanceState()
3) 로컬 저장소 활용
이번 포스팅에서는 간단한 정보에 대해서 임시적으로 저장 후 UI 복원을 한 것이기 때문에 onSaveInstanceState를 활용했습니다.
Bundle 만으로도 충분히 onDestroy() 전에 데이터를 유지한채 재생성시 전달하고 그 값에 맞춰 UI를 재구성하는 방법
또한 간단하지만서도 중요한 개념이라고 생각합니다. 감사합니다.
'Android' 카테고리의 다른 글
Layer / DatePickerDialog / Spinner / SharedPreference (6) | 2024.07.22 |
---|---|
Xml - style 구성 / Intent 관리 (0) | 2024.07.21 |
Kotiln 기초 3편 (3) | 2024.07.16 |
Kotlin 기초 2편 (0) | 2024.07.11 |
Kotlin 기초 1편 (0) | 2024.07.11 |