240126 TIL #305 Android #18 Activity

김춘복·2024년 1월 26일
0

TIL : Today I Learned

목록 보기
305/571

Today I Learned

오늘은 안드로이드 액티비티 공부!


Activity

Intent

컴포넌트 간에 작업을 수행하도록 사용되는 메시징 객체

  • 기능을 수행하는 클래스가 아니라 데이터(컴포넌트 실행 정보)를 담는 클래스

  • 같은 앱의 컴포넌트 뿐만 아니라 외부 앱의 컴포넌트와 연동시 중재하는 역할을 수행한다.

  • startActivity()

    MainActivity에서 DetailActivity로 넘어가야된다고 가정하면, Main에서 detail 객체를 생성하는게 아니라, intent를 통해 시스템에 정보를 전달해주고 시스템이 detail 객체를 실행해주는 개념이다.

// MainActivity에서 DetailActivity를 실행시키는 코드
val intent: Intent = Intent(this, DetailActivity::class.java)
startActivity(intent)
  • 액티비티, 서비스, 브로드캐스트리시버, 콘텐츠 프로바이더는 매니페스트 파일에 태그로 등록이 되고 name 속성이 지정되어야 한다.
    시스템이 런타임때 매니페스트 파일을 참조해 컴포넌트를 파악하고 인텐트를 통해 컴포넌트를 사용할 수 있기 때문이다.

Extra Data

  • 인텐트를 이용해 데이터를 전달해야할 때 사용한다.
    엑스트라 데이터는 인텐트에 담는 부가정보이다.

  • 인텐트에 엑스트라 데이터를 추가하는 함수는 putExtra()이다.
    첫 매개변수는 데이터의 식별자, 두번째 매개변수가 전달할 데이터이다.

val intent: Intent = Intent(this, DetailActivity::class.java)
intent.putExtra("data1", "hello")
intent.putExtra("data2", 100)
startActivity(intent)
  • 인텐트로 전달받은 데이터를 가져오는 함수는 get타입Extra()이다.
val data1 = intent.getStringData("data1")
// 2번째 매개변수는 defaultValue
val data2 = intent.getIntData("data2",0)

액티비티 전환 사후처리

한 액티비티에서 인텐트를 이용해 다른 액티비티로 넘어가고, 그 액티비티에서 작업이 끝나 다시 원래 액티비티로 돌아오면 사후 처리가 필요한 경우가 있다.

  • startActivity(Intent intent)
    사후처리가 필요없을 때 사용

  • startActivityForResult(Intent intent, int requestCode)
    사후처리가 필요할때 사용하나 안드로이드 11버전 이후로는 잘 쓰지않음
    requestCode는 개발자가 정한 요청코드로 인텐트 식별값이다.

  • ActivityResultLauncher
    최근 권장되는 사후처리 방식이다.

Contract

  • ActivityResultLauncher로 실행될 요청을 처리하는 역할
    실제 인텐트를 발생시키는 역할을 한다.

  • Contract는 ActivityResultContract를 상속받은 서브클래스로 직접 만들거나 API에서 제공하는 클래스를 이용해도 된다.

ActivityResultLauncher

resisterForActivityResult() 함수로 만드는 객체로 함수의 매개변수에 실제 작업자인 Contract 객체와 결과를 처리하는 Collback 객체를 등록해준다.

launch

ActivityResultLauncher의 함수로 호출 순간 ActivityResultLauncher에 등록된 Contract 객체가 실행된다.

val requestLauncher : ActivityResultLauncher<Intent> = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()) // contract
    // Callback
    { 
    val resultData = it.data?.getStringExtra("result")
    binding.mainResultView.text = "result : $resultData"
}

val intent: Intent = Intent(this, DetailActivity::class.java)
requestLauncher.launch(intent) // launch() 함수로 실행

암시적 인텐트

  • 명시적 인텐트
    앞의 예시처럼 DetailActivity::class.java 처럼 클래스 타입 레퍼런스를 이용한 것을 명시적 인텐트라고 한다.

  • 외부 앱의 컴포넌트는 이렇게 클래스 타입 레퍼런스를 활용할 수 없다. 따라서 암시적 인텐트를 사용한다.

  • 암시적 인텐트는 매티페스트 파일에 선언된 intent-filter를 이용한다.

<activity android:name=".OneActivity"/>
<activity android:name=".TwoActivity"
          android:exported="true">
  		  <intent-filter>
            	<action android:name="ACTION_EDIT />
          </intent-filter>
</activity>
  • 어떤 액티비티를 매니페스트 파일에 등록할 때 name 속성만 지정하면 명시적 인텐트로만 실행할 수 있다. (OneActivity)

  • 외부에서도 인텐트로 실행할 수 있게 하려면 <intent-filter>를 액티비티,서비스,리시버 등의 컴포넌트 등록 태그 하위에 작성하면 된다.

  • 인텐트로 실행할 액티비티가 없을 상황을 대비해 예외처리를 해야한다.

val intent = Intent("ACTION_HELLO")
try {
	startActivity(intent)
} catch (e: Exception) {
	Toast.makeText(this, "no app...", Toast.LENGTH_SHORT).show()
}
  • 인텐트로 실행할 액티비티가 시스템에 여러개라면 사용자가 선택하는 하나만 실행된다. ex) 지도앱 실행시 여러 지도앱중 선택
    특정앱의 액티비티를 실행하고 싶으면 setPackage() 함수로 앱의 식별자를 지정하면 된다.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:37.1234, 123.4567"))
intent.setPackage("com.google.android.apps.maps")
startActivity(intent)

인텐트 하위 필터

intent-filter 하위에는 다음과 같은 하위 태그를 이용해 정보를 설정할 수 있다.
<action> : 컴포넌트의 기능을 나타내는 문자열
<category> : 컴포넌트가 포함되는 범주를 나타내는 문자열
<data> : 컴포넌트에 필요한 정보. URL 형식으로 표현


액티비티 생명 주기

생명주기는 액티비티가 생성되어 소멸하기까지의 과정이다.
Activity 클래스는 액티비티가 상태변화를 알아차릴 수 있는 여러가지 콜백함수를 제공한다.

액티비티의 상태는 아래의 3가지로 구분할 수 있다.

  1. 활성
    액티비티 화면이 출력되고있고 사용자가 이벤트를 발생시킬 수 있는 상태
    액티비티가 포커스를 가진 상태
    onCreate()->onStart()->onResume() 함수까지 호출된다.
    setContentView() 함수로 출력한 내용이 액티비티 화면에 나온다.
    onCreate()는 최초 한번만 호출되고, s랑 r은 반복 호출이 가능하다.
  1. 일시 정지
    액티비티 화면은 출력되고있지만 사용자가 이벤트를 발생 시킬 수 없는 상태
    onPause() 함수까지 호출된 상태
    액티비티가 화면엔 보이지만 포커스를 잃은 상태다.
    다시 포커스를 얻으면 onResume() 함수가 자동으로 호출된다.

  2. 비활성
    액티비티의 화면이 출력되고 있지 않는 상태
    액티비티가 종료되진 않고 화면에만 보이지 않는 상태.
    onPause()->onStop()함수까지 호출된다.

  • 종료는 onDestroy()까지 호출되었다는 의미이다.


액티비티 상태 저장

  • 일반적으로 액티비티가 종료되면 객체가 소멸되므로 액티비티의 데이터는 모두 소멸되고 재실행시 초기상태가 된다.

  • 그래서 액티비티가 종료될 때 유지해야할 데이터를 저장했다가 다시 실행할 때 복원할 수 있다.

  • 앱이 종료될 경우의 데이터는 DB와 같은 데이터 영속화를 사용하면 된다. 하지만 앱이 아닌 액티비티가 의도치 않게 종료될 수가 있다.

  • 화면 회전을 하게되면 액티비티가 종료되었다가 다시 시작되는데, 생명주기는 onResume()까지 호출 된 액티비티에서 화면 회전이 발생하면 onPause() -> onStop() -> onDestroy()까지 호출되어 종료된다.

  • 그 후 시스템에서 다시 onCreate() -> onStart() -> onResume()까지 호출하고, 화면은 다시 나타나지만 만약 onResume()이 실행된 후에 발생한 이벤트나 네트워킹에 의해 데이터가 발생하였다면 모두 유실된다.

Bundle

액티비티 종료시 저장했다가 복원할 데이터는 Bundle 객체에 담는다.

  • onCreate(), onSaveInstanceState(), onRestoreInstanceState() 함수는 매개변수로 Bundle 객체를 가진다.
  • onSaveInstanceState()는 onStop() 함수 다음에 호출되므로 이 함수가 호출되면 액티비티가 종료된다. putString(), putInt() 같은 함수를 이용해 데이터를 저장한다.
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("data1", "hello")
        outState.putInt("data2", 100)
    }
}
  • onRestoreInstanceState()에서 getInt() 같은 함수로 번들 객체를 통해 데이터를 가져온다.
    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        val data1 = savedInstanceState.getString("data1")
        val data2 = savedInstanceState.getInt("data2")
    }

태스크 관리

  • 태스크 관리 : 액티비티를 어떻게 생성하고 관리하는지를 제어하는 일

  • 액티비티 태스크 : 앱이 실행될 때 시스템에서 액티비티의 각종 정보를 저장하는 공간

태스크 제어

  • 매니페스트 파일의 <activity> 태그의 launchMode를 이용한다.
    이 액티비티는 인텐트에 의해 항상 설정한대로 생성되어 태스크에 등록된다.
<activity android:name".TwoActivity" android:launchMode=singleTop">
  • 다른방법으로는 코드에서 인텐트를 발생 시키기 전에 flags 속성에 설정한다.
    이 방법은 인텐트가 발생할 때에 한번만 적용되어 태스크에 등록도니다.
val intent = Intent(this, TwoActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)
  • launchMode에 설정할 수 있는 속성값은 standard, singleTop, singleTask, singleInstance가 있다.

  • standard
    기본값. 각 액티비티 인스턴스는 새로운 태스크에 생성되며, 이전 인스턴스와는 독립적이다. 여러 인스턴스가 동시에 실행될 수 있습니다.

  • singleTop
    액티비티가 현재 태스크의 맨 위에 있는 경우 새로운 인스턴스를 생성하지 않고 재사용한다. 새로운 인스턴스가 필요한 경우에만 새로 생성된다.

  • singleTask
    액티비티가 존재하면 해당 인스턴스를 사용하고, 그렇지 않은 경우에만 새로운 인스턴스를 생성한다. 해당 액티비티 위에 다른 액티비티가 존재하면 그 위의 액티비티를 모두 제거한다.

  • singleInstance
    액티비티가 새로운 태스크에 생성되며, 이 태스크에 해당하는 유일한 액티비티 인스턴스가 된다. 이 액티비티와 관련된 다른 액티비티들은 별도의 태스크에 속하게 된다.


ANR 문제

Activity Not Response, 액티비티가 응답하지 않는 오류
액티비티가 사용자 이벤트에 5초간 반응하지 않으면 오류가 발생한다.

  • 시스템에서 액티비티를 실행하는 수행 흐름을 메인스레드 or UI스레드라고 한다.

  • 액티비티에서 일반적으로 시간이 가장 소요되는 작업은 서버와 통신하는 네트워크이다. 모바일환경에선 특히 더 불안정하다.
    대부분 전문 라이브러리(Volley or Retrofit2)를 사용해 만들어 이때는 개발자가 ANR 문제를 고려하지 않아도 된다.

  • ANR을 해결하는 방법은 액티비티를 실행한 메인 스레드 이외에 개발자 스레드(실행 흐름)을 따로 만들어 시간이 오래 걸리는 작업을 담당하게 한다.
    하지만 이방법으로는 화면을 변경할 수 없는 문제가 생긴다. 화면 변경은 메인스레드에서만 할 수 있기 때문이다.

코루틴

위의 문제를 해결하기 위해 코틀린에서 제공하는 코루틴(Coroutine)기능을 이용한다.

  • Coroutine
    non-blocking lightweight thread. 비동기 경량 스레드
    코루틴은 스레드보다 가벼우면서 더 많은 기능을 제공한다.
    경량에 메모리 누수가 적고 취소등 다양한 기능을 지원하며 많은 제트팩 라이브러리에 적용되어 있다.

  • 의존성 등록

implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
  • 스코프(scope)
    코루틴을 구동하려면 스코프를 준비해야 한다. 스코프는 성격이 같은 코루틴을 묶는 개념이다. 한 스코프에 여러 코루틴을 구동할 수 있고 한 어플에 여러 스코프를 만들 수 있다.
val channel = Channel<Int>()
val backgroundScope = CoroutineScope(Dispatchers.Default + Job())
backgroundScope.launch { 
    var sum = 0L
    var time = measureTimeMillis { 
        for (i in 1..2_000_000_000){
            sum += 1
        }
    }
    Log.d("event", "time : $time")
    channel.send(sum.toInt())
}
val mainScope = GlobalScope.launch(Dispatchers.Main) { 
    channel.consumeEach { 
        binding.resultView.text = "sum : $it"
    }
}
  • 위 코드에서 backgroundScope와 mainScope를 만들었다. 디스패처는 이 스코프에서 구동한 코루틴이 어디에서 동작해야하는지를 나타낸다.
    Dispatchers.Main : 액티비티의 메인 스레드에서 동작하는 코루틴을 생성
    메인스레드에서 동작하므로 UI변경이 가능하지만 빨리 끝나는 작업을 맡기는게 좋다.
    Dispatchers.IO : 파일에 읽거나 쓰기 or 네트워크 작업에 최적화
    Dispatchers.Default : CPU를 많이 사용하는 작업을 백그라운드에서 실행

  • 코루틴의 값을 전달받는 방법으로 사용하는 Channel은 큐와 비슷하며 send() 함수로 데이터를 전달하면 데이터를 받는 코루틴에선 receive()나 consumeEach() 등의 함수로 데이터를 받는다.

profile
Backend Dev / Data Engineer

0개의 댓글