Chapter 8. 액티비티 컴포넌트¶
(여기 확인 해야됨!)
'깡샘의 안드로이드 앱 프로그래밍 with 코틀린' 13장 학습 내용
1절. 인텐트
2절. 액티비티 생명주기
3절. 액티비티 ANR 문제
4절. 코루틴
5절. ToDoList 앱 구현
프로젝트 바로가기
1절. 인텐트¶
개념¶
- 컴포넌트 실행을 위해 시스템에 전달하는 메시지
- Android 컴포넌트 클래스
- 개발자가 코드에서 직접 생성 후 실행 불가
- 시스템에서 인텐트 정보 분석 후 맞춤형 컴포넌트 실행
- 외부 앱 컴포넌트와 연동할 경우에도 마찬가지
- startActivity() : 인텐트를 시스템에 전달
- Intent 생성자 매개변수 : 클래스 타입 레퍼런스 정보
<!-- MainActivity의 DetailActivity 등록 -->
<activity
android:name=".DetailActivity"
android:exported="true" />
<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>
인텐트 엑스트라 데이터¶
- 인텐트에 담는 부가 정보
- 인텐트에 컴포넌트 실행 요청 시 데이터를 함께 전송하려고 할 경우 이용
| 함수 종류 | 설명 |
|---|---|
| putExtra() | 인텐트에 엑스트라 데이터를 추가하는 함수 |
| getIntExtra() | 데이터를 추가로 가져오는 인텐트 객체 함수 |
// 엑스트라 데이터 추가
val intent: Intent = Intent(this, DetailActivity::class.java)
intent.putExtra("data1", "hello")
intent.putExtra("data2", 10)
startActivity(intent)
// 엑스트라 데이터 반환
val data1 = intent.getStringExtra("data1")
val data2 = intent.getIntExtra("data2", 0)
액티비티 화면 전환¶
| 인텐트로 액티비티 시작하는 방법 |
|---|
| public void startActivity(Intent intent) |
| ActivityResultLauncher(권장) |
ActivityResultLauncher¶
- registerForActivityResult() 함수로 만드는 객체
-
함수의 매개 변수에 실제 작업자 Contract 객체와 결과를 처리하는 Callback 객체 등록
-
Contract
- ActivityResultLauncher로 실행될 요청을 처리하는 역할
- ActivityResultContract를 상속받은 서브 클래스
| 종류 | 설명 |
|---|---|
| PickContact | 선택한 연락처 Uri 획득 |
| RequestPermission | 권한 요청 · 허락 여부 파악 |
| RequestMultiplePermissions | 여러 권한 동시 요청 |
| StartActivityForResult | 인텐트 발생 · 액티비티 실행 결과 획득 |
| TakePicturePreview | 사진 촬영 후 비트맵 획득 |
| TakePicture | 사진 촬영·저장·비트맵 획득 |
- launch
- 함수 호출 시 ActivityResultLauncher에 등록된 Contract 객체 실행
// ActivityResultLauncher 생성
val requestLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult())
// Contract
{
// Callback
val resultData = it.data?.getStringExtra("result")
binding.mainResultView.text = "result : $resultData"
}
// ActivityResultLauncher 실행
val intent: Intent = Intent(this, DetailActivity::class.java)
requestLauncher.launch(intent)
- finish() : 자동 화면(결과) 전환할 경우 이용하는 함수
인텐트 필터¶
- 하위 태그 정보 설정
| 태그 종류 | 설명 |
|---|---|
| 컴포넌트의 기능 표현 문자열 | |
| 컴포넌트가 포함되는 범주 표현 문자열 | |
| 컴포넌트에 필요한 데이터 정보 |
<!-- 자동 생성 메인 액티비티 -->
<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>
<!-- 외부 앱과 연동하는 인텐트 필터 설정(메인 액티비티 X)-->
<activity
android:name=".TwoActivity"
android:exported="true" >
<intent-filter>
<action android:name="ACTION_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity>
// 인텐트 프로퍼티 이용
val intent = Intent()
intent.action = "ACTION_EDIT"
intent.data = Uri.parse("http://www.google.com")
startActivity(intent)
// 인텐트 생성자 이용 방법
val intent = Intent("ACTION_EDIT", Uri.parse("http://www.google.com"))
startActivity(intent)
<!-- mineType 설정 -->
<activity android:name=".TwoActivity">
<intent-filter>
<action android:name="ACTION_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mineType="image/" />
</intent-filter>
</activity>
- 실행할 컴포넌트 정보를 어떻게 설정하는가?
<!-- 암시적·명시적 인텐트 -->
<activity android:name=".OneActivity" />
<activity
android:name=".TwoActivity"
android:exported="true" >
<intent-filter>
<action android:name="ACTION_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
명시적 인텐트(Explicit Intent)¶
- 클래스 타입 레퍼런스 정보 활용 인텐트
암시적 인텐트(Implicit Intent)¶
- 인텐트 필터 정보를 활용한 인텐트
- AndroidManifest.xml 파일에 선언된 인텐트 필터 사용
액티비티 인텐트 동작 방식¶
| 액티비티 개수 | 시스템 처리 |
|---|---|
| 0게 | 인텐트를 시작한 곳에 오류 발생 |
| 1개 | 문제없이 실행 |
| n개 | 사용자 선택으로 하나만 실행 |
// 해당 액티비티가 없는 경우 예외 처리
val intent = Intent("ACTION_HELLO")
try {
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, "no application", Toast.LENGHT_SHORT).show()
}
// 액티비티가 여러 개인 경우
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:38.7749, 127.4194"))
startActivity(intent)
// 패키지 지정
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:37.7749, 127.4194"))
intent.setPackage("com.google.android.apps.maps")
startActivity(intent)
2절. 액티비티 생명주기¶
액티비티 상태¶
- 생명 주기 : 액티비티의 생성 ~ 소멸 과정
- 액티비티 종료 : onDestroy()까지의 호출을 의미
활성 상태¶
- 액티비티가 실행되어 화면에 나오고 사용자 이벤트 처리 가능 상태
- 처음 실행된 액티비티는 onCreate() → onStart() → onResume() 함수까지 호출
일시 정지 상태¶
- 액티비티가 화면에 보이지만 포커스를 잃어 사용자 이벤트를 처리할 수 없는 상태
- onPause() 함수까지 호출된 상태
비활성 상태¶
- 액티비티가 종료되지 않고 화면에만 보이지 않는 상태
- 활성 상태에서 비활성 상태가 되면 onPause() → onStop() 함수까지 호출
액티비티 상태 저장¶
- 액티비티 종료 : 객체 소멸 -> 액티비티의 데이터 모두 소멸
- 상태 저장 : 액티비티 종료로 메모리 데이터가 사라져도 재실행 시 사용자가 저장한 데이터로 액티비티의 상태를 복원하겠다는 의미
- 화면 전환 : 액티비티 종료 후 출력 -> 즉, 액티비티의 데이터 초기화
Bundle¶
- 액티비티 종료 후 저장했다가 복원할 데이터가 있는 경우 이용하는 객체
// 번들 객체 사용 예시
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle){
super.onRestoreInstanceState(savedInstanceState)
}
override fun onSavedInstanceState(outState: Bundle?){
super.onSavedInstanceState(outState)
}
- Bundle에 데이터를 담으면 자동으로 데이터를 파일로 저장
// 번들에 데이터 저장
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSpring("data1", "hello")
outState.putInt("data2", 10)
}
- 액티비티가 다시 생성되어 실행될 때 캐싱파일이 있으면 그 내용을 번들 객체에 담아 onCreate(), onRestoreInstanceState() 함수의 매개 변수로 전달
// 번들에 저장된 데이터 반환
override fun onRestoreInstanceState(savedInstanceSttate: Bundle){
super.onRestoreInstanceState(savedInstanceSttate)
val data1 = savedInstanceState.getString("data1")
val data2 = savedInstanceState.getInt("data2")
}
3절. 액티비티 ANR 문제¶
ANR 문제¶
- 액티비티가 응답하지 않는 오류 상황
- 메인 스레드 : 시스템에서 액티비티를 실행하는 수행 흐름
- UI 스레드 : 화면을 출력하는 수행 흐름
해결 방법 (1) : 개발자 스레드 분리¶
- 액티비티를 실행한 메인 스레드 외의 실행 흐름(개발자 스레드)을 따로 생성 후 시간이 오래 걸리는 작업 담당
- 이 방법으로 대체할 경우 ANR 오류는 해결되지만 화면을 변경할 수 없다는 문제 발생
4절. 코루틴¶
해결 방법 (2) : 코루틴¶
코루틴¶
- 비동기 경량 스레드
- 프로그래밍 언어에서 제공하는 기능
| 장점 |
|---|
| 경량 |
| 적은 메모리 누수 |
| 취소 등 다양한 기능 지원 |
| 많은 제트팩 라이브러리에서 적용 |
안드로이드에서의 코루틴 이용¶
ANR 오류 발생 코드¶
// 오랜 시간 소모 작업 예시
var sum = 0L
var time = measureTimeMillis{
for(i in 1..2_000_000_000){
sum += i
}
}
Log.d("Yunseo", "time : $time")
binding.resultView.text = "sum : $sum"
- ANR 오류 해결을 위한 스레드-핸들러 구조
// 스레드 - 핸들러 구조 코드
val handler = object:Handler() {
override fun handleMessage(msg: Message){
super.handleMessage(msg)
binding.resultView.text = "sum : ${msg.arg1}"
}
}
thread{
var sum = 0L
var time = measureTimeMillis {
for(i in 1..2_000_000_000) {
sum += i
}
val message = Message()
message.arg1 = sum.toInt()
handler.sendMessage(message)
}
Log.d("Yunseo", "time : $time")
}
코드의 코루틴화¶
- 코루틴 구등을 위해서 스코프 준비
- 스코프에서 코루틴 구동
코루틴 스코프¶
- CoroutineScope를 구현한 클래스 객체
- 직접 구현 가능
- 코틀린 언어 제공 스코프 이용
| 코틀린 제공 스코프 종류 |
|---|
| GlobalScope |
| ActorScope |
| ProducerScope |
// 코루틴 코드
// 백그라운드에서 동작(시간이 오래 걸리는 작업)
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 += i
}
}
Log.d("Yunseo", "time : $time")
channel.send(sum.toInt())
}
// 메인 스레드에서 동작(화면에 결괏값 표시)
val mainScope = GlobalScope.launch(Dispatchers.Main) {
channel.consumeEach {
binding.resultView.text = "sum : $it"
}
}
Dispatcher¶
- 스코프에서 구동한 코루틴이 어디에서 동작해야 하는지 표시
| 종류 | 설명 |
|---|---|
| Dispatchers.Main | 액티비티의 메인 스레드에서 동작하는 코루틴 생성 |
| Dispatchers.IO | 파일에 읽기·쓰기 또는 네트워크 작업 등에 최적화 |
| Dispatchers.Default | CPU를 많이 사용하는 작업을 백그라운드에서 실행 |
5장. ToDoList 앱 구현¶
1) 모듈 생성 & 빌드 그래들 설정¶
- Ch13_Activity 모듈 생성
- 뷰 바인딩 사용 설정
2) 할 일 등록 액티비티 생성¶
- New → Activity → Empty Activity → 액티비티 이름에 AddActivity 입력
3) 리소스 & 소스 파일 복사¶
- res 디렉터리 아래에 drawable, layout, menu 디렉터리를 현재 모듈의 res 디렉터리로 복사
- 소스가 든 디렉터리에서 AddActivity.kt, Main Activity.kt, MyAdapter.kt 파일을 현재 모듈의 소스 영역에 복사
4) 할 일 등록 액티비티 작성¶
- AddActivity.kt 파일을 열어 내용을 추가
5) 메인 액티비티 작성¶
- MainActivity.kt 파일을 열고 내용을 추가