will come true

[Android/Kotlin] Activity 실행 후 결과 받기, startActivityForResult() @Deprecated 대안 Activity Result API 사용법 본문

Android

[Android/Kotlin] Activity 실행 후 결과 받기, startActivityForResult() @Deprecated 대안 Activity Result API 사용법

haehyun 2022. 1. 26. 17:28

한 앱에서 MainActivity와 DetailActivity 두 개의 액티비티가 존재한다고 가정했을 때, MainActivity에서 DetailActivity를 실행하며 데이터A를 넘기고, DetailActivity는 자기자신을 종료하면서 자신을 호출했던 MainActivity에 데이터 B를 넘기는 걸 Activity Result API를 사용해서 구현해 볼 것이다.

*Activity : 안드로이드 4대 컴포넌트 중 앱에서 화면을 구성하는 컴포넌트. (1액티비티=1화면)

1. 액티비티 컴포넌트 생성

[File] > [Activity] > [Empty Activity]

MainActivity는 모듈 생성시 자동으로 하나 만들어지기 때문에, DetailActivity만 추가하면 된다.
1액티비티는 1화면이기 때문에, 액티비티를 만들면 DetailActivity.kt 코드 파일, activity_detail.xml 레이아웃 파일이 한 쌍 생성된다. 

 

2. 매니페스트 등록

Manifest.xml 파일

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.ch16_provider">

    <uses-permission android:name="android.permission.READ_CONTACTS" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidLab">
        <activity
            android:name=".DetailActivity"
            android:exported="false" />
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

프로그래밍에 사용할 컴포넌트(Activity, BroadcastReceiver, Service, Content Provider)는 모두 매니페스트 파일(AndroidManifest.xml)에 등록해야 한다. 인텐트를 시스템에 전달해서 컴포넌트를 실행할 때, 시스템이 제일 먼저 보는 것이 매니페스트 파일이기 때문이다. 시스템은 이 매니페스트 파일을 보고 어떤 컴포넌트들이 있는지 인식한다.

컴포넌트 매니페스트 태그
Activity <activity></activity>
Broadcast Receiver <receiver></receiver>
Service <service></service>
Content Provider <provider></provider>

 

1번 과정처럼 컴포넌트를 생성하면 자동으로 매니페스트 파일에 추가되지만, 차후 암시적 인텐트를 위한 <intent-filter>등 항목은 사용자가 직접 추가해야 한다.

*Intent : 컴포넌트를 실행하는 정보, 이 정보가 담긴 인텐트 객체를 시스템에 전달하면 컴포넌트가 실행된다.

 

3. 컴포넌트 정보를 담은 인텐트를 시스템에 전달한다.

실행될 액티비티에 데이터를 일방적으로 전하기만 할거라면(=단방향 작업) 아래와 같이 인텐트에 엑스트라 데이터(Extra Data)를 추가한 뒤 startActivity() 함수로 인텐트를 전달하면 되지만, 실행한 액티비티가 종료될 때 메인 액티비티로 데이터를 반환받고 싶다면 다른 함수를 써야 한다.

[MainActivity.kt] : DetailActivity 실행, 데이터 송신

val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("data1", "hello world")
intent.putExtra("data2", 10)
startActivity(intent)

[DetailActivity.kt] : MainActivity 데이터 수신

val intent = intent
val data1 = intent.getStringExtra("data1")
val data2 = intent.getIntExtra("data2")

 

4. @Deprecated 이슈

실행한 액티비티에서 결과와 화면을 기존 액티비티로 되돌려야 할 경우(=양방향 작업) startActivityForResult() 함수를 쓰면 됐지만, startActivityForResult() 함수는 현재 @Deprecated 되어 사용을 권장하지 않고 있다. 
(@Deprecated : 중요도가 떨어지거나 문제점이 발견된 탓에 다른 차선책을 내놨으니, 이 함수는 더 이상 사용하지말라는 뜻)

Deprecated 된 이유는 startActivityForResult() 와 onActivityResult() API는 모든 API 레벨의 Activity 클래스에서 사용할 수 있지만, 안드로이드 스튜디오 측에서 AndroidX Activity와 Fragment에 도입된 Activity Result API 사용을 권장하기 때문이다. 간단히 말해 startActivityForResult()에서 일어나던 문제를 해결한 새로운 API가 있으니 그걸 사용하라는 뜻.
Activity Result API는 시스템에서 결과가 전달되면 이를 등록, 실행, 처리하기 위한 구성요소를 제공한다.

 

5. Activity Result API

결과를 얻기 위해 Activity를 실행할 때, 메모리 부족으로 인해 프로세스와 액티비티가 소멸될 수 있다. (ex: A액티비티에서 B액티비티를 실행했는데, B액티비티 실행 중 메모리 부족으로 인해 A액티비티가 종료됨. B액티비티가 작업을 끝내고 A액티비티로 돌아왔을 때는 이미 A액티비티가 종료됐다가 재실행된 터라 B액티비티를 실행했다는 사실 자체를 기억 못하는 상태가 됨. 결국 B액티비티에서 전달한 값을 받을 수 없게되는 것)

이를 위해, Activity Result API는 다른 액티비티를 실행하는 코드 위치에서 결과 콜백을 분리한다. (결과 콜백은 프로세스와 액티비티를 다시 생성할 때 사용할 수 있어야 함)

타입 이름 설명
메서드 registerForActivityResult() 결과 콜백을 등록한다.
ActivityResultContract 및 ActivityResultCallback을 가져와서 다른 액티비티를 실행하는 데 사용할 ActivityResultLauncher를 반환한다.
클래스 ActivityResultContract<I, O> 결과를 생성하는 데 필요한 입력 타입과 결과의 출력 타입을 정의한다. 카메라, 권한 요청 등 기본 인텐트 작업의 기본 제약(contract)을 제공한다.
I : 입력 타입
O : 출력 타입
createIntent(Context context, I Input) : startActivityForResult() 에 사용할 수 있는 인텐트를 생성한다.
인터페이스 ActivityResultCallback<O> ActivityResultContract에 정의된 출력 타입의 객체를 가져오는 onActivityResult(O result) 메서드가 포함된 단일 메서드 인터페이스. Activity Result를 이용할 수 있을 때 호출하는 타입 안정성 콜백.
O : 반환 타입
추상 클래스 ActivityResultLauncher<I> ActivityResultContract 실행 프로세스를 시작하기 위한 실행기?
I : 실행에 필요한 입력 타입

*registerForActivityResult(ActivityResultContract<I, O>, ActivityResultCallback<O>) → ActivityResultLauncher<I>

 

5-1. Activity Result에 콜백 등록

registerForActivityResult() 함수의 매개변수로 ActivityResultcontract, ActivityResultCallback을 전달해서 힘수 호출 결과로 ActivityResultLauncher 인스턴스를 얻어낸다. registerForActivityResult()는 콜백을 등록하는 함수일 뿐, 실제 액티비티 실행이나 결과 요청은 여기서 반환된 ActivityResultLauncher 인스턴스가 담당한다.

val getResultText = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    // Handle the returned result
}

 

5-2. 결과를 위한 액티비티 실행

ActivityLauncher<I>는 ActivityResultContract<I> 타입과 일치하는 입력을 가져온다. launch() 함수를 호출하면 결과를 생성하는 프로세스가 시작된다. 사용자가 후속 활동을 완료하고 반환하면 ActivityResultCallback의 onActivityResult() 함수가 실행된다.

lateinit var binding: ActivityMainBinding

val getResultText = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    // Handle the returned result
}


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.button.setOnClickListener {
        val intent = Intent(this, DetailActivity::class.java)
        intent.putExtra("data1", "hello world")
        intent.putExtra("data2", 10)
        getResultText.launch(intent)
    }
}

 

5-3. 테스트

MainActivity에서 전달한 데이터를 DetailActivity에서 받아서 사용, DetailActivity에서 MainActivity로 데이터를 전달한 다음 액티비티 종료. 전달받은 결과값을 MainActivity에서 사용. (=두 액티비티 간의 데이터 전달 구현)

[MainActivity.kt]

package com.example.ch16_provider

import ...

class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding
    lateinit var getResultText: ActivityResultLauncher<Intent>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // ActivityResultLauncher 초기화, 결과 값 이벤트 핸들러 정의
        getResultText = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                val data = result.data?.getStringExtra("result")
                binding.textView.text = data
            }
        }

        binding.button.setOnClickListener {
            // 인텐트 실행
            val intent = Intent(this, DetailActivity::class.java)
            intent.putExtra("data1", "hello world")
            intent.putExtra("data2", 10)
            getResultText.launch(intent)
        }
    }
}

 

[DetailActivity.kt]

package com.example.ch16_provider

import ...

class DetailActivity : AppCompatActivity() {
    lateinit var binding: ActivityDetailBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityDetailBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val intent = intent
        binding.textView.text = intent.getStringExtra("data1")

        binding.button.setOnClickListener {
            intent.putExtra("result", "happy coding")
            setResult(RESULT_OK, intent)
            if(!isFinishing) finish()
        }
    }
}

 

실행 결과

 


<코틀린 문법>

고차함수(high order function)

함수를 값처럼 취급하여 매개변수나 반환값으로 주고받는 함수. (=람다식 활용)
함수를 매개변수나 반환값으로 이용할 수 있다는 것은 함수를 변수에 대입할 수 있기 때문이다.

위에서 사용된 registerForActivityResult() 함수가 고차함수이다. 함수 호출 결과로 반환된 함수를 getContent 변수에 대입하고 있는 것이다.

val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) {
    // Handle the returned Uri
}

 

코틀린에서는 호출하려는 함수가 고차 함수(high order function)이고 마지막 전달 인자가 람다식이면 소괄호를 생략해도 된다.

위에서 사용된 registerForActivityResult() 함수는 원래였다면 아래와 같이 익명 메서드 형태로 작성되었어야 하나,

val getContent = registerForActivityResult(ActivityResultContracts.GetContent(), fun(it: Uri) {
    // Handle the returned Uri
})

 

매개변수임을 표시하는 소괄호를 생략하고 람다 함수 형태를 취해 아래와 같은 형태가 된 것이다. registerForActivityResult() 메서드가 매개변수를 하나만 받고 끝난 것 같아보이지만, 뒤에 중괄호로 { } 감싸인 부분까지 registerForActivityResult()의 매개변수 부분이다. 즉, 매개변수로  ①ActivityResultContracts.GetContent() 반환 값②ActivityResultCallback<Uri?> 타입의 람다식 두 인자를 전달받는 코드이다.

val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
    // Handle the returned Uri
}

 

registerForActivityResult() 함수의 매개변수는 차례대로 ActivityResultContract<I, O>, ActivityResultCallback<O> 타입이고 반환값은 ActivityResultLauncher<I>타입이다. 참고로 여기서 ActivityResultContract<I, O>는 제네릭으로 I, O타입 받아서, I타입 매개변수를 사용해 O타입 객체를 반환하는 클래스이다. 자바에서 함수형 인터페이스 Function<T, R>을 생각하면 될 것 같다. (*Function<T, R> : 하나의 T타입 매개변수를 받아서 R타입 결과를 반환.) ActivityResultCallback<O> 역시 출력타입 제네릭을 받는 단일 메서드 인터페이스이기 때문에 람다식으로 바로 익명 메서드를 구현해주면 된다. 

 


참고자료

Comments