Android 공부 (2)

백상휘·2025년 10월 3일
0

액티비티 생명주기

안드로이드 SDK 의 생명주기 메소드들을 이용해 이에 적합한 작업을 수행할 수 있도록 코드를 작성할 수 있다. 각각 생명주기의 콜백함수를 잘 이용해야 한다.

  • onCreate(savedInstantceState: Bundle?) : 가장 많이 사용.

    • 일반적으로 한 번만 호출되지만 액티비티 재생성 시 다시 호출된다.
    • 레이아웃 표시를 준비하는 코드를 작성한다. 메소드가 반환되고 나서 바로 UI 가 표시되는 건 아니다.
    • 보통 setContentView(R.layout.activity_main) 호출을 통해 초기화 작업을 시작한다.
    • savedInstanceState 매개변수는 액티비티 재생성 간 데이터 유지를 위해 사용한다. 키-값 쌍.
  • onRestart() : 액티비티가 다시 시작될 때 onStart() 직전 호출된다.

    • 액티비티를 다시 시작한다는 것은 홈 버튼을 눌러 액티비티가 백그라운드로 이동한 후 다시 포어그라운드로 돌아올 때 이다.
    • 기기 회전 등 구성 변경이 일어나면 액티비티가 재생성 된다.
      • 화면 회전 시 기기 구성 변경으로 인한 액티비티 재생성이 일어나지 않게 하려면 AndroidManifest.xml 파일의 MainActivity 에 android:configCHanges="orientation|screenSize|screenLayout" 을 추가한다.
      • 액티비티를 다시 시작하지 않는 방식은 권장되지 않는다. 시스템이 자동으로 대체 리소스를 적용하지 않기 때문이다.
    • 액티비티 다시 시작, 재생성은 다른 얘기다.
  • onStart() : 액티비티가 백그라운드에서 포어그라운드로 이동할 때 수행되는 첫 번째 콜백이다.

  • onRestoreInstanceState(savedInstanceState: Bundle?) : savedInstanceState 로 상태를 저장한 경우 onStart 이후에 호출되는 메서드. onCreate(savedInstanceState:Bundle?) 말고도 여기서 상태를 복원할 수도 있다.

  • onResume() : 액티비티 생성 마지막 과정에서 호출되는 콜백. 백그라운드에서 포어그라운드로 돌아올 때 실행.

    • 이 메소드를 끝으로 UI 가 표시되고 이벤트를 받을 준비가 완료된다.
  • onSaveInstanceState(outState: Bundle?) : 액티비티 상태를 저장하기에 좋은 함수다. (액티비티 deinit 혹은 백그라운드 상태변경 시 호출되는 함수인 듯)

  • onPause() : 액티비티 백그라운드 전환 또는 대화상자나 다른 액티비티가 나타날 때 호출.

  • onStop() : 액티비티가 가려질 때 호출. 액티비티는 백그라운드.

  • onDestroy() : 시스템 자원이 부족할 시 안드로이드가 자동으로 액티비티를 deinit(finish 함수 호출).

  • 액티비티 시작 -> onCreate() -> onStart() -> (액티비티 화면 표시) -> onResume() -> (액티비티 동작 및 실행) -> (홈버튼 눌러서 액티비티 보이지 않음) -> onStop() -> (다시 앱 실행) -> onRestart() -> onStart, onResume, 액티비티 실행, onPause, onStop -> (앱 종료) -> onDestroy() -> (액티비티 종료)

package com.example.activitycallback

import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import android.util.Log

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "onCreate")
    }

    override fun onRestart() {
        super.onRestart()
        Log.d(TAG, "onRestart")
    }

    override fun onStart() {
        super.onStart()
        Log.d(TAG, "OnStart")
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        Log.d(TAG, "OnRestoreInstanceState")
    }

    override fun onResume() {
        super.onResume()
        Log.d(TAG, "onResume")
    }

    override fun onPause() {
        super.onPause()
        Log.d(TAG, "onPause")
    }

    override fun onStop() {
        super.onStop()
        Log.d(TAG, "onStop")
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        Log.d(TAG, "onSaveInstanceState")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy")
    }

    companion object {
        private const val TAG = "MainActivity"
    }
}

액티비티 상태 저장 및 복원

기기 회전이 구성 변경을 일으켜 액티비티를 재생성한다고 배웠다. 메모리 확보를 위해 액티비티 종료가 발생하기도 한다.

그 때문에 액티비티 상태를 보존하고 복원하는 것이 중요하다.

android:textSize 의 단위를 sp 로 지정하는데, 이는 밀도 독립적인 픽셀을 나타낸다. 앱이 실행되는 기기의 밀도에 따라 크기를 정의하되 사용자 설정에 따라 텍스트 크기도 변경 가능하다.

layout xml 파일에서 안드로이드 프레임워크는 id 가 지정된 경우에만 상태를 저장한다.

onSaveInstanceState 에서 Bundle 값을 키-값 형태로 저장하고 onRestoreInstanceState 나 onCreate 에서 다시 가져와 사용한다. 간단한 로직이라면 onCreate 에서, 아니면 onRestoreInstanceState 에서 복구한다.

Bundle 을 이용하는 방법도 있지만 안드로이드 프레임워크에서 제공하는 안드로이드 아키텍처 컴포넌트(AAC, Android Architecture Component) 중 하나인 ViewModel 을 이용해 상태를 저장하고 복원할 수도 있다.

인텐트와의 액티비티 상호작용

안드로이드에서 인텐트는 컴포넌트 간의 통신 메커니즘이다. 앱을 제작할 때 대부분의 경우 현재 액티비티에서 어떤 동작이 발생하면 특정한 다른 액티비티가 시작되기를 원할 수 있다.

정확히 어떤 액티비티가 시작될지 지정하는 걸 명시적 인텐트라고 한다.

묵시적 인텐트의 예시는 카메라이다. 카메라 시작 인텐트를 보내고 시스템이 그걸 처리하는 방식이다.

인텐트 관련 이벤트에 응답하려면 인텐트 필터를 등록해야 한다. 인텐트 필터는 AndroidManifest.xml 의 intent-filter 태그를 사용한다.

<intent-filter>
  <action android:name="android.intent.action.MAIN" />
  <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

android.intent.action.MAIN 는 앱의 주 진입점을 말한다. android.intent.category.LAUNCHER 는 앱이 런처에 표시되게 해준다. 둘을 합치면 런처에서 앱 아이콘을 클릭할 때 앱이 실행된다는 의미다.

실제 앱을 만들어서 인텐트를 이용해 두 액티비티가 상호작용하는 과정을 설명한다.

const val RAINBOW_COLOR_NAME = "RAINBOW_COLOR_NAME"
const val RAINBOW_COLOR = "RAINBOW_COLOR"
const val DEFAULT_COLOR = "#FFFFFF"
class MainActivity : AppCompatActivity() {
    private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        activityResult -> val data = activityResult.data
        val backgroundColor = data?.getIntExtra(
            RAINBOW_COLOR,
            Color.parseColor(DEFAULT_COLOR))
            ?: Color.parseColor(DEFAULT_COLOR)

        val colorName = data?.getStringExtra(RAINBOW_COLOR_NAME) ?: ""
        val colorMessage = getString(R.string.color_chosen_message, colorName)

        val rainbowColor = findViewById<TextView>(R.id.rainbow_color)
        rainbowColor.setBackgroundColor(ContextCompat.getColor(this, backgroundColor))
        rainbowColor.text = colorMessage
        rainbowColor.isVisible = true
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.submit_button)
            .setOnClickListener {
                startForResult.launch(
                    Intent(
                        this,
                        RainbowColorPickerActivity::class.java))
            }
    }
}

class RainbowColorPickerActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_rainbow_color_picker)

        val colorPickerClickListener = View.OnClickListener { view ->
            when (view.id) {
                R.id.red_button -> setRainbowColor(
                    getString(R.string.red),
                    R.color.red)
                R.id.orange_button -> setRainbowColor(
                    getString(R.string.orange),
                    R.color.orange)
                R.id.yellow_button -> setRainbowColor(
                    getString(R.string.yellow),
                    R.color.yellow)
                R.id.green_button -> setRainbowColor(
                    getString(R.string.green),
                    R.color.green)
                R.id.blue_button -> setRainbowColor(
                    getString(R.string.blue),
                    R.color.blue)
                R.id.indigo_button -> setRainbowColor(
                    getString(R.string.indigo),
                    R.color.indigo)
                R.id.violet_button -> setRainbowColor(
                    getString(R.string.violet),
                    R.color.violet)
                else -> {
                    Toast.makeText(this,
                        getString(R.string.unexpected_color), Toast.LENGTH_LONG
                    ).show()
                }
            }
        }

        findViewById<View>(R.id.red_button).setOnClickListener(colorPickerClickListener)
        findViewById<View>(R.id.blue_button).setOnClickListener(colorPickerClickListener)
        findViewById<View>(R.id.yellow_button).setOnClickListener(colorPickerClickListener)
        findViewById<View>(R.id.green_button).setOnClickListener(colorPickerClickListener)
        findViewById<View>(R.id.blue_button).setOnClickListener(colorPickerClickListener)
        findViewById<View>(R.id.indigo_button).setOnClickListener(colorPickerClickListener)
        findViewById<View>(R.id.violet_button).setOnClickListener(colorPickerClickListener)
    }

    private fun setRainbowColor(colorName: String, color: Int) {
        Intent().let { pickedColorIntent ->
            pickedColorIntent.putExtra(RAINBOW_COLOR_NAME, colorName)
            pickedColorIntent.putExtra(RAINBOW_COLOR, color)
            setResult(Activity.RESULT_OK, pickedColorIntent)
            finish()
        }
    }
}

registerForActivityResult 를 이용해서 액티비티의 상호작용을 미리 정의해 놓는다. registerForActivityResult 내에는 activityResult.data 를 통해 원시타입 데이터들을 사용할 수 있다. 키는 미리 상수로 정의해 두었다. 이를 통해 넘어온 데이터로 뷰의 속성을 새로 정의하고 있다.

registerForActivityResult 로 만들어지는 ActivityResultLauncher 의 launch 함수를 이용해 인텐트를 정의한다. Intent(this, RainbowColorPickerActivity::class.java) 는 새로운 액티비티를 생성한다는 의미이다. MainActivity.kt 는 이쯤 보면 될 것 같다.

RainbowColorPickerActivity.kt 는 setRainbowColor 가 중요하겠다. Intent() 를 이용해 MainActivity 에서 생성한 인텐트를 불러오고 전달된 String, Int 를 putExtra 함수로 추가하고 있다. 세팅이 끝났다면 setResult 로 결과 반환 및 finish 로 액티비티를 종료시키고 이전 액티비티를 보여주게 한다.

인텐트, 태스크 및 런치 모드

앱을 런처에서 열면 앱은 자체적인 Task 를 생성하고 생성한 각 액티비티는 Back Stack 에 추가된다. 여기서 동작방식이 액티비티 런치 모드에 따라 달라진다.

아래 3개는 일반적으로 사용하지는 않는다.

  • standard : 사용자가 3 개의 동일한 액티비티를 차례로 열도록 작업을 했다면 뒤로가기 3번 눌렀을 때 이전 화면/액티비티로 이동한 후 기기의 홈 화면으로 돌아가지만 여전히 앱은 실행돼 있는 상태를 유지한다.
  • singleTop : 백스택 최상단에 위치한 액티비티가 또 실행되면 onNewIntent 콜백을 실행하며 기존의 액티비티 인스턴스를 재활용한다.
  • singleTask
  • singleInstance
  • singleInstancePerTask

Activity 를 추가하게 되면 AndroidManifest.xml 에 activity 태그가 추가되는데 여기에 속성으로 android:launchMode="singleTop" 을 주게 되면 singleTop 모드가 된다. 기본은 당연히 standard.

profile
plug-compatible programming unit

0개의 댓글