Compose 기본사항 코드랩 바로가기

CodeLab 원본영상


<영상 내용 요약 >

1. Jetpack Compose 는 최신 Native UI 도구 키트입니다.

2. (글 작성일 기준) Compose 안정화버전은 1.3.0 까지 출시되었습니다.

3. 아직 실무에 Compose 를 도입하지 않았다면, JetPack Compose 를 준비하기에는 정말 좋은 시기 입니다.

4. Compose 는 직관적이고, 개발 가속화를 위하여 설계되었습니다. (훨씬 적은 코드로 UI 를 개발할 수 있습니다.)

5. 기기성능은 향상되고, 앱에 대한 기대가 커진만큼 UI는 훨씬 동적이고, 표현이 풍부해졌습니다.

6. Views 로도 충분히 만들어낼 수 있지만, 최신 아키텍처를 기반으로 하고 있는 Kotlin 을 활용하는 현대적 도구키트를 원하는 의견으로 인해 Jetpack Compose 가 등장했습니다.

7. Jetpack Compose 는 선언적 독립형 도구 키트입니다.

8. 선언적(Declarative) : 요즘 앱은 데이터가 동적이고, 실시간으로 업데이트 됩니다. 기존에는 xml 에 UI 를 선언하고, 데이터가 바뀌면 UI 도 업데이트 해주어야 합니다. 변형도 필요합니다. 이 과정을 위하여 View를 조회하고, 속성을 설정해야 합니다. 애플리케이션 상태가 바뀔때마다 (데이터베이스, 네트워크 호출 로드, 사용자 상호작용 등) 새로운 정보를 이용하여 UI 를 업데이트하여 데이터를 동기화해야합니다. View 마다 상태가 다르고, 각각 업데이트 해야하므로 그 과정이 복잡하고, 모델과 UI 를 동기화 하는 과정에서 버그가 엄청나게 발생할 수 있습니다. 이는 온전히 개발자가 책임지고 모든걸 업데이트 해야합니다. 앱과 UI 가 복잡해진 만큼, 당연히 오류가 생기기가 더 쉬워졌습니다. 

Compose 는 xml 방식과 다른 방식을 사용합니다. Compose 는 "상태를 UI 로 변환합니다." UI 는 변경이 불가능하고, 한번 생성되면 업데이트가 불가능합니다. 앱상태가 바뀌면, 새로운 상태를 새로운 표현으로 변환합니다. 상태에 따른 데이터 동기화 문제가 완전히 해결됩니다. UI 전체를 다시 생성하기 때문입니다. 물론, Compose 는 매우 지능적이고 효율적이어서 변경되지 않은 요소에 대한 작업은 건너뜁니다. 그러나 개념적으로 특정 상태에 따라 UI 를 새로 생성하는 것과 같습니다.

코드는 그저 특정 상태에 따른 UI 형태를 설명할뿐, 생성 방법을 지정하지 않습니다.

Q. 어떻게 상태를 UI 로 변환할까?
A. Compose 에서 UI 구성요소는 구성가능한 주석이 달린 함수일 뿐입니다. 그래서 UI 구성 요소를 빠르고 쉽게 생성 할 수 있습니다.

따라서, 재사용가능한 요소로 구성된 라이브러리로 UI 를 나누는 것이 좋습니다. (중복코드 방지)

@Composable
fun MessageList(messages: List<String) {
    Column {
        messages.forEach { message ->
            Text(text = message)
        }
    }
}

위 함수는 값을 반환하는 것이 아닌, UI 를 전달합니다. (Compose 라이브러리의 Column 과 Text Composable 사용합니다.)

Text 를 수직으로 배열하고, 간단한 Text Lable 을 표시합니다.

만약, 특정 조건에 따라서 요소를 표시하려면, 즉 message 가 없을때 Lable 을 표현하고 싶다면, if 문을 추가하면 됩니다.

@Composable
fun MessageList(messages: List<String) {
    Column {
        if (messages.size == 0) {
            Text(text = "No messages")
        } else {
            messages.forEach { message ->
                Text(text = message)
            }
        }
    }
}

Composable 은 매개변수를 받을 수 있고, 사실 받아야 합니다.

데이터를 UI 로 변환한다는 것이 이런 뜻입니다. Composable 은 데이터를 함수 매개변수로 받아서 UI 를 전달합니다.

이렇게되면, UI 가 동기화 상태에서 벗어나지 않습니다. (메시지가 없다는 Text 를 삭제하지 않는 등의 실수를 하지 않을 수 있습니다.)
-> 메시지데이터가 있다면, 메시지가 없는 UI 를 제거하고, 메시지가 있는 UI 를 보여주어야하는데, 둘다 보여주는 실수를 하지 않을 수 있다. 는 것입니다.
상태가 바뀌었을때, 이 함수를 실행한다면 새로운 UI 가 생성됩니다. 이러한 행위를 리컴포징(recomposing)이라고 합니다.

메시지 목록은 어떻게 바뀌는가?

-> 콜 스택을 처리하는 동안 ViewModel 이 메시지의 LiveData 를 노출합니다. 이 데이터를 관찰할 수 있고, message 필드를 읽는 Composable 은 새로운 데이터가 입력될때마다 리컴포저블(recomposable) 됩니다

직접 감시 객체를 설정할 필요가 없습니다. Compose 컴파일러는 어느 Composable 이 상태를 읽는지 추적하고, 상태가 바뀌면 자동으로 다시 실행합니다. 여기서 Compose 가 지능적이기 때문에 입력이 변경된 Composable 만 다시 실행하고, 나머지는 건너뜁니다.

@Composable
fun ConversationScreen() {
    val viewModel: ConversationViewModel = viewModel()
    val messages by viewModel.messages.observeAsState()
    MessageList(messages)
}

@Composable
fun MessageList(messages: List<String) {
    ...
}

위 코드를 보면 이전과 달린 viewModel.messages.observe { ... } 는 필요없고, Compose 컴파일러가 observeAsState() 로 된 messages 를 추적하기 때문에, 해당 상태가 바뀌면 자동으로 ConversationScreen() 를 다시 실행하고, 이 과정에서 입력이 변경된 MessagesList(message: List<String) 도 다시 실행됩니다. 만약, 변경된 데이터가 없는 Composable 함수는 건너뛰는 것으로 이해하면 될 것 같습니다.

 

각 Composable 은 변경할 수 없습니다. Composable 을 참조하거나, 나중에 쿼리하거나, 내용을 업데이트 할 수 없습니다.

정보입력을 원할땐, 모두 매개변수로 Composable 에 전달해야 합니다.

 

하지만, 모든 Composable 이 고정되어 있는 것은 아닙니다. 아래 코드가 그 예시입니다.

@Composable
fun MessageList(messages: List<String>) {
    Column {
        CheckBox(
            checked = false
        )
        ...
    }
}

위 코드에서 CheckBox 를 클릭해도 View 와 달리 시각적인 변화가 없습니다.

상태를 나타내는 checked 가 상수로 전달되었기 때문입니다.

여기서 checked 의 값을 결정하는 로컬변수를 넣어보면 시각적인 변화를 줄 수 있습니다.

@Composable
fun MessageList(messages: List<String>) {
    Column {
        var selectAll: Boolean = ...
        CheckBox(
            checked = selectAll,
            onCheckChange = { checked ->
                selectAll = checked
            }
        )
        ...
    }
}

CheckBox 에는 onCheckChange 이벤트를 제공합니다. 이 콜백을 통하여 로컬 상태를 업데이트 할 수 있습니다.

이렇게 하면 Compose 컴파일러가 상태를 읽을 때, checkBox 를 다시 받습니다.

처음에는, 코드를 작성해야 선택이 된다는 것이 직관적이지 못하다고 생각할 수 있지만 이것이 선언적UI 의 핵심 개념입니다.

 

위 코드를 "단일 진실 공급원" 으로 로직을 올릴 수도 있습니다.

@Composable
fun MessageList(
    messages: List<String>,
    selectAll: Boolean,
    onSelectAll: (Boolean) -> Unit
) {
    Column {
        CheckBox(
            checked = selectAll,
            onCheckChange = onSelectAll
        )
        ...
    }
}

 

선언적 UI 의 핵심은, 특정 상태에서 UI의 형태를 완전히 설명하고, 상태가 바뀌면 프레임워크에서 UI 업데이트를 처리합니다.

 

Compose 는 여러가지 애플리케이션 아키텍처와 호환되지만, 단반향 데이터 플로우를 아키텍처와 잘 맞습니다.

-> ViewModel 이 화면 상태의 단일 스트림을 노출하면, Compose UI 에서 관찰하고, 각 구성요소의 매개변수로 전달합니다.

각 구성요소는 필요한 상태만 수신하므로, 데이터를 바꿀때만 업데이트하면 됩니다.

 

LiveData<ScreenState> 처럼 ViewState 객체의 단일 스트림을 생성하면, 상태변경을 한곳에서 처리하는 데 도움이 됩니다.

전체적 화면 상태를 추론하고 오류를 낮추기 쉽습니다.

 

이렇게되면, 입력에 따라 완전히 제어되기 때문에 간단하게 Composable 을 테스트 할 수 있습니다. 

 

9. Jetpack Compose 는 머티리얼 디자인 구성요소와 테마시스템을 구현합니다.

애플리케이션을 어셈블하는데 필요한 구성 요소도 제공합니다. Buttons, Cards, FABs, AppBars 등이 이에 해당합니다.

모든 구성요소는 기본으로 머티리얼 스타일링을 따르고, 머티리얼 테마를 구현하기 때문에 모든 구성요소를 브랜드에 맞게 체계적으로 맞춤 설정할 수 있습니다. 원하는 색, 도형, 서체 스타일 등을 지정할 수 있습니다.

 

10. Compose 는 간단하지만, 강력한 새로운 레이아웃 시스템을 제공합니다. Row, Column 은 기존의 Linear Layout 과 비슷하다고 보면 되고, Box 는 Frame Layout 과 비슷하다고 보면 됩니다.

그러나 View 시스템과 다르게, Compose 레이아웃 모델은 여러 척도를 전달할 수 없어서 중첩된 레이아웃에 적당합니다.

 

11. 새로운 Compose DSL 을 적용한 ConstraintLayout 을 사용하면, 더욱 복잡한 레이아웃을 표현할 수 있지만,

맞춤형 레이아웃도 훨씬 간단하게 구현됩니다.

 

12. 가장 기대가 큰 개선 사항은 새로운 애니메이션 시스템 입니다.

훨씬 간단하게 사용할 수 있고, 보다 효과적이고 간단하게 UI 에 모션을 적용할 수 있습니다.

 

13. Compose에서는 테스트와 접근성이 일급객체 입니다. UI 에 병렬트리를 제공하는 시맨틱 시스템을 기반으로 합니다.

 

14. Compose 는 테스트 기능을 극대화하는 전용 테스트 아티팩트를 제공하고, 독립적으로 Composable 을 테스트하는 간편한 API를 제공합니다. 테스트에서 Clock 이 노출되고, 그후 UI 가 업데이트되고, 이를 제어하기 위한 API 가 제공됩니다.

 

< 실제 완성된 UI 를 어떻게 작성했는지 보고 싶다면, 영상 11분 부터 보시면 됩니다. >

 

15. Compose 는 기존 View 시스템과 호환됩니다. 필요에 따라 Compose 를 점진적으로 도입할 수 있습니다.

View 시스템 뿐만 아니라, Navigation, ViewModel, LIveData, Rx, Flow, Paging, Hilt 등 다른 주요 라이브러리와의 통합을 제공합니다.

AppCompat XML 테마를 Compose 로 변환할때는 adapter 를 제공합니다.

 

 

'BackUp (관리중지) > Android 이론' 카테고리의 다른 글

이제는, SharedPreferences 👉 DataStore  (0) 2021.05.16
Android Service  (0) 2021.05.14
Android Coroutine [코루틴]  (0) 2021.05.04
[Android Jetpack] LiveData란,  (0) 2021.04.21
Fragment 와 Fragment LifeCycle 분석  (0) 2021.04.19

https://developer.android.com/topic/libraries/architecture/datastore

 

SharedPreferences

SharedPreferences 는 키-값 의 데이터를 저장하기 위해 사용합니다. 이때, 저장은 File 에 이루어집니다.

File 에 저장을 하고 읽는 다는 것은, 데이터를 유지할 수 있다는 것을 의미합니다.

 

저는 "앱 사용자설정" 등의 설정값들을 저장하기 위해 많이 사용합니다.


DataStore

DataStore 도 키-값 의 데이터를 저장하기 위해 사용합니다. 역시나 저장은 File 에 이루어집니다.

 

그렇다면 왜 DataStore 를 사용하는 것이 더 좋을까요?

(반대의견이 있을 수도 있다고... 생각...합니다만.. 일단 작성해보겠습니다.)

 

1. DataStore 는 코틀린 코루틴을 사용한 Flow 를 사용하여 비동기적으로 저장됩니다.

(SharedPreferences 는 읽는 용도로 사용될때 비동기를 지원하였습니다.)

 

2. DataStore 는 내부적으로 DisPatchers.IO 로 동작하고 있기 때문에 MainThread 로부터 안전합니다.

 

위 두 내용만 본다면, 음... 그다지.. 그냥 기존에 쓰던걸 쓰면 안되나? 그게 더 편할듯.. 이라고 생각하실 수 있습니다.


Preferences DataStore vs Proto DataStore

 

Preferences DataStore

- 위에 말한 두가지의 장점을 포함하여 SharedPreferences 보다 몇가지 개선된 점을 갖고 있는 DataStore 입니다.

 

Proto DataStore 

- 이것이 진짜입니다.

1. Protocol buffers 를 이용하여 객체를 저장할 수 있다.

(Preferences 는 객체를 저장할 수 없기 때문에 직렬화와 역직렬화를 위해 번거로운 작업이 있었습니다.)

 

2. 타입안전성을 제공합니다. 

Preferences DataStore 역시 타입에 대한 안전성을 제공하지는 않았습니다. 그러나, Proto DataStore 는, 1번에 작성 된 것처럼

Protocol buffers 를 사용하기 때문에 타입에 대한 안전성을 제공합니다.

 

3. Key 를 사용할 필요가 없습니다.

Proto DataStore 는 지정되어있는 타입을 인식하고 제공하기 때문에 Key 를 사용할 필요가 없습니다.


SharedPreferences VS PreferencesDataStore vs ProtoDataStore

 

https://developer.android.com/codelabs/android-preferences-datastore#3

 

 

 

참고 https://developer.android.com/topic/libraries/architecture/datastore

 

Datastore  |  Android 개발자  |  Android Developers

Datastore   Android Jetpack의 구성요소. Jetpack Datastore는 프로토콜 버퍼를 사용하여 키-값 쌍 또는 유형이 지정된 객체를 저장할 수 있는 데이터 저장소 솔루션입니다. Datastore는 Kotlin 코루틴 및 Flow를

developer.android.com

'BackUp (관리중지) > Android 이론' 카테고리의 다른 글

Android CodeLab [Compose 개요]  (2) 2022.11.12
Android Service  (0) 2021.05.14
Android Coroutine [코루틴]  (0) 2021.05.04
[Android Jetpack] LiveData란,  (0) 2021.04.21
Fragment 와 Fragment LifeCycle 분석  (0) 2021.04.19

서비스(Service)

서비스(Service)는 사용자와 상호 작용하지 않고 더 오래 실행되는 작업을 수행하려는 응용 프로그램 구성 요소 또는 다른 응용 프로그램이 사용할 수 있는 기능을 제공하는 응용 프로그램 구성 요소이다.

사용자와 상호 작용하지 않고 : UI 가 존재하지 않습니다.
더 오래 실행되는 작업을 수행 : 백그라운드에서 작업을 수행합니다.
다른 응용 프로그램이 사용할 수 있는 : A 앱에서 B 앱의 기능을 사용할 수 있습니다. (B앱의 서비스가 이를 제공해야함)

* 서비스는 별도의 프로세스가 아니다.
- 서비스를 백그라운드에서 돌릴 수 있지만, 서비스 자체가 별도의 프로세스는 아닙니다. 따로 지정하지 않는 이상, 서비스는 앱의 프로세스와 동일한 프로세스에서 동작합니다.

* 서비스는 쓰레드가 아니다.
- 흔히 앱에서 백그라운드쓰레드로 작업을 처리하는 경우가 있는데,
서비스는 단지, 백그라운드에게 "수행해야 할 작업" 이 있다 를 "알리는" 것일 뿐입니다.

* 서비스는 프로세스 내의 주 쓰레드에서 동작하기 때문에, MP3 재생 등과 같은 기능을 하려면 별도의 쓰레드를 서비스 내에 만들어 주어야 한다.

서비스 LifeCycle

  • onStartCommand() 시스템이 이 메서드를 호출하는 것은 또 다른 구성 요소(예: 액티비티)가 서비스를 시작하도록 요청하는 경우입니다. 이때 startService()를 호출하는 방법을 씁니다. 이 메서드가 실행되면 서비스가 시작되고 백그라운드에서 무한히 실행될 수 있습니다. 이것을 구현하면 서비스의 작업이 완료되었을 때 해당 서비스를 중단하는 것은 개발자 본인의 책임이며, 이때 stopSelf() 또는 stopService()를 호출하면 됩니다. 바인딩만 제공하고자 하는 경우, 이 메서드를 구현하지 않아도 됩니다.
  • onBind() 시스템은 다른 구성 요소가 해당 서비스에 바인딩되고자 하는 경우(예를 들어 RPC를 수행하기 위해)에도 이 메서드를 호출합니다. 이때 bindService()를 호출하는 방법을 사용합니다. 이 메서드를 구현할 때에는 클라이언트가 서비스와 통신을 주고받기 위해 사용할 인터페이스를 제공해야 합니다. 이때 IBinder를 반환하면 됩니다. 이 메서드는 항상 구현해야 하지만, 바인딩을 허용하지 않으려면 null을 반환해야 합니다.
  • onCreate() 시스템은 서비스가 처음 생성되었을 때(즉 서비스가 onStartCommand() 또는 onBind()를 호출하기 전에) 이 메서드를 호출하여 일회성 설정 절차를 수행합니다. 서비스가 이미 실행 중인 경우, 이 메서드는 호출되지 않습니다.
  • onDestroy() 시스템이 이 메서드를 호출하는 것은 서비스를 더 이상 사용하지 않고 소멸시킬 때입니다. 서비스는 스레드, 등록된 리스너 또는 수신기 등의 각종 리소스를 정리하기 위해 이것을 구현해야 합니다. 이는 서비스가 수신하는 마지막 호출입니다.

Context  |  Android 개발자  |  Android Developers

developer.android.com

왓챠 과제전형을 진행하면서, 코루틴을 사용하는 도중에 lifecycle 과 coroutine 동작에 대한 학습이 부족함을 느꼈습니다.

그래서 다시한번 Coroutine 에 대해 정리를 하고자 합니다.

 

코루틴은 코틀린때문에 등장한 것이 아니라고 합니다.

코루틴의 개념은 훨씬 이전부터 존재해왔으며 (언제부터일까... 그것까진 안찾아봤어요.. )

오히려 저같은 후세대 개발자들은 안드로이드에서 코틀린을 공식언어로 적용하면서 만났을 것이라고 생각합니다.

 

 

1. MainRoutine 과 SubRoutine

코루틴에 대해서 알기 전에, 메인루틴과 서브루틴에 대해서 알아야 합니다.

메인루틴은 흔히, 메인메소드라고 생각하면 됩니다.

서브루틴은 메인함수 내에서 동작하는 메소드라고 생각하시면 됩니다.

 

    private fun mainRoutine() {
        val nameA = subRoutine1()
        val nameB = subRoutine2()
        val nameC = subRoutine3()
    }

    private fun subRoutine1() : String {
        return "subRoutine1"
    }
    private fun subRoutine2() : String {
        return "subRoutine1"
    }
    private fun subRoutine3() : String {
        return "subRoutine1"
    }

위 코드는 mainRoutine() 에서 순차적으로 subRoutine1() -> subRoutine2() -> subRoutine3()

으로 동작합니다.

 

순차적으로 동작하는 만큼, subRoutine1() 에서 while(true) 에 break; 가 없다면, subRoutine2() 과 subRoutine3()은 

절대 동작할리가 없겠죠? 만약 while 탈출 조건이 있다고 해도, 탈출하기 전까지는 다음 코드가 실행되지 않습니다.

 

이게 왜 중요한가???? 

Thread 동작

하나의 쓰레드 내부코드는 위에서 설명한 것처럼 모두 순차적으로 진행됩니다.

멀티쓰레드는 내부코드가 여러개로 나뉘는 것이 아닌, 순차적으로 진행되는 각 쓰레드를 여러개 두고 돌아가며 실행시키는 것이라고 보면 됩니다.

 

 

Routine 이 무엇인지 알았나요?

MainRoutine 내부에 SubRoutin, 그 안에 SubRoutine, 그 안에 SubRoutine ... 이렇게 존재한다고 하면,

결국 마지막 SubRoutine 의 코드부터 끝이 나야 그 다음 Soutine 들이 종료가 가능한거죠.. 음?.. 어...?

스택이 떠오르네요. Routine 은 스택으로 동작하는 군요!

 

어찌보면, 당연한 흐름이겠습니다만, 이런식으로 깊게 깊게 들어가면 끝이 없겠...네요... 이친구는 여기까지!

코루틴은 무엇일까?

코루틴은 시작부터 종료까지 suspend 와 resume 이 여러번 호출될 수 있습니다.

이게 무엇이냐면,

subRoutineA 가 실행된중에 잠시 suspend 하여 탈출하고, subRoutineB 를 실행하다가 subRoutineB 를 끝내거나,

subRoutineB 도 잠시 중지한다음 subRoutineA 를 다시 실행할 수 있다는 것입니다.

 

* 핵심은 return 이 아니기 때문에 subRoutineA 가 중지된 부분부터 이어서 실행이 가능하다는 것이죠!

 

    private fun mainRoutine() {
        subRoutine1()
        subRoutine2()
        subRoutine3()
    }

    private fun subRoutine1() {
        var count = 0
        while(count<5) {
            Thread.sleep(500)
            println("subRoutine1")
            count += 1
        }
    }

    private fun subRoutine2() {
        var count = 0
        while(count<5) {
            Thread.sleep(500)
            println("subRoutine2")
            count += 1
        }
    }

    private fun subRoutine3() {
        var count = 0
        while(count<5) {
            Thread.sleep(500)
            println("subRoutine3")
            count += 1
        }
    }

이 코드는 코루틴이 아닌 일반적인 코드입니다. 해당 코드들은 어떻게 동작할지 상상이 가죠?

 

그렇다면 다음 코드를 볼까요?

    private fun mainRoutine() {
        MainScope().launch {
            println("Start subRoutine1")
            subRoutine1()
        }

        MainScope().launch {
            println("Start subRoutine2")
            subRoutine2()
        }

        MainScope().launch {
            println("Start subRoutine3")
            subRoutine3()
        }
    }

    private suspend fun subRoutine1() {
        var count = 0
        while(count<5) {
            delay(500)
            println("subRoutine1")
            count += 1
        }
    }

    private suspend fun subRoutine2() {
        var count = 0
        while(count<5) {
            delay(500)
            println("subRoutine2")
            count += 1
        }
    }

    private fun subRoutine3() {
        var count = 0
        while(count<5) {
            Thread.sleep(500)
            println("subRoutine3")
            count += 1
        }
    }

subRoutine1, 2, 3 은 모두 코루틴으로 실행을 시켰습니다.

 

그럼, 1,2,3,1,2,3,1,2,3,1,2,3,1,2,3, 순으로 돌까요?

 

정답은 틀렸습니다.

 

subRoutineA 가 실행된중에 잠시 suspend 하여 탈출하고, subRoutineB 를 실행하다가 subRoutineB 를 끝내거나,

subRoutineB 도 잠시 중지한다음 subRoutineA 를 다시 실행하려면

 

suspend 로 메소드를 작성해야합니다.

 

즉, subRoutineA 와 subRoutineB 는 서로 잠시 탈출하고, 다음대상실행, 탈출 , 실행 으로 동잡합니다만,

 

subRoutineC 는 suspend 키워드가 없기 때문에  선점한 이후에 탈출하려면 subRoutineC 가 종료되어야만 가능합니다.

 

따라서 실행결과는 아래와 같습니다.

 

이러한 동작은 멀티쓰레드로 만들어낼수도 있습니다.

그러나, 엄청나게 큰 차이는

 

멀티쓰레드 : 쓰레드간 컨텍스트 스위칭으로 서로 다르게 동작

코루틴 : 단일쓰레드에서 함수를 잠시 일시정지하고 다른 함수를 실행하는 식으로 동작

 

Suspend 메소드는 어떤식으로 동작할까?

 

아래와 같은 suspend 메소드를 변경시켜보겠습니다.

정확하게 suspend 가 변하는 부분은 훨씬 복잡할 것입니다.

어떤 메소드를 사용할지 결정하는 등에 대한 부분도 고려가 될것으로 판단되구요..

중요한건 결국 메소드를 스위칭 한다는 것이죠

즉, 1과 2가 서로 어떻게 돌아가면서 실행되어야 하는데요,

 

따라서 아래와 같이 변경해 보았습니다.

label 을 두고, 현재 메소드 실행 이후에 어떤 메소드를 실행할기 결정시키는 것인데요, 공부한바에 의하면

코루틴의 suspend 는 저런식으로 label 가지고 결정한다고 합니다. 물론 훨씬 복잡하겠지만요 :)

 

멀티쓰레드 vs 코루틴  누가 좋을까?

 

멀티쓰레드를 스위칭하는 작업은 코루틴에서 함수를 스위칭하는것보다 훨씬 많은 메모리를 필요로 합니다.

즉, 사용 메모리 : 멀티쓰레드 > 코루틴

 

그러나, 코루틴은 단일쓰레드 내에서 여러개를 번갈아가며 실행시키는 것입니다.

(이는 단일코어에서 멀티쓰레드를 번갈아가며 실행시키는 것과 유사하다고 봐야할것 같습니다.)

 

즉 10개의 Task 가 있다면,

 

1. 단일코어 멀티쓰레드 : 1개의 코어에 10개의 쓰레드를 동작시키고...

  - 처리속도

멀티쓰레드의 컨텍스트 스위칭 시간 vs 코루틴 labeling 시간 중 누가 더 빠른가?

를 봐야할 것 같습니다. 개인적으로는 최소 같거나, 코루틴이 더 빠를것이라고 생각합니다.

 

2. 멀티코어 멀티쓰레드 : 3개의 코어가 3,3,4 의 쓰레드를 동작시키고...

 - 처리속도

멀티코어 멀티쓰레드 > 코루틴

 

그러나, 이 부분은 좀더 깊이있게 공부해서 어떤것이 좋은지 확인이 필요할것 같습니다.

제가 아직 이 판단은 잘 서지 않네요.. 

그러나, 쓰레드를 정말 제대로 잘짜는게 아니라면 코루틴이 좋지 않을까 싶...흡...

GlobalScope vs MainScope vs lifecycleScope 등등등

안드로이드에서 코루틴을 사용하다보면,

 

~~~Scope.launch {

...

}

 

이런식으로 많이 사용을 하게 되는데요.

대체 어떤 스코프를 써야하는지에 대한 생각이 들었던 적이 있었습니다. 다시한번 코루틴을 정리한 이유도 이때문이구요.

 

1. GlobalScope : 자체적인 쓰레드풀을 하나 갖고 있는 곳에서 실행됩니다.

즉 정말, Global 한 영역에서 동작시킨다고 봐야합니다.

MainActivity 에서 호출한다면, Application 이 해당 코루틴의 동작구간이 된다고 봐야합니다.

 

2. MainScope : MainThread 에 해당하는 영역에서 실행됩니다.

MainActivity 에서 호출한다면, MainActivity 가 해당 코루틴의 동작구간이 된다고 봐야합니다.

 

3. lifecycleScope : 호출되는 라이프사이클에 맞게 실행됩니다.

 

NullPointException 의 고통을 받고 싶지 않다면, 사용되는 코루틴이 LifeCycle 에 맞게 생성, 제거 될 수 있도록 신경을 써야 할 것 같습니다.

 

즉, 저는 앞으로 앱을 개발할 때, lifecycleScope 를 제일 많이 사용하게 될 것 같고, 그러지 못하는 경우에는 cancel 을 적절하게 사용하고, 제대로 동작할 수 있도록 고민하며 개발을 진행해야 한다고 판단했습니다.

Github 에서 많은 코드들, 더 좋은 코드를 작성하기 위한 고민을 하다보면 LiveData 가 등장하게 됩니다.

 

1. LiveData 란 무엇일까요?

일단, LiveData 는 Android Jetpack 의 Component 입니다.

* Android Jetpack : Jetpack은 개발자가 관심 있는 코드에 집중할 수 있도록 권장사항 준수, 상용구 코드 제거, 모든 Android 버전과 기기에서 일관되게 작동하는 코드 작성을 돕는 라이브러리 모음입니다.

 

오늘은 문서를 따라가면서 정리를 먼저 해보겠습니다. ( 영어고수가 아닌 저는 번역본과 원문을 같이 봐야합니다.. )

ps. 저처럼 영어에 약하신 분들은, 번역본만 보시지 마시고, 꼭 원문과 같이보세요! ( 자칫 잘못하면 이해를 방해합니다. )

LiveData is an observable data holder class.

LiveData는 관측 가능한 데이터 홀더 클래스입니다.

 

Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services.

일반 관측 가능과 달리 LiveData는 수명주기 인식 기능을 갖추고 있습니다.

따라서 Activity, Fragment 또는 services와 같은 다른 앱 구성요소의 수명주기를 고려합니다.

 

This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

수명주기 인식을 통해 LiveData는 활동상태에 있는 앱 구성 요소 관찰자만 업데이트할 수 있습니다.

 

구구절절하게 써놨지만, 정리하자면...

LiveData 는 Activity,Fragment, Services 과 같은 LifeCycle 가 고려되어 동작한다.

- STARTED, RESUMED : LiveData 활동상태 , Observer 객체 업데이트 가능
- 그 외 : LiveData 비활동상태, Observer 객체 업데이트 불가능

- DESTROYED : LiveData Observer 제거 가능

 

라고 볼 수 있을 것 같습니다.


2. LiveData 를 사용했을 때 장점은 무엇일까요? 

1. UI 와 데이터 상태의 일치를 보장할 수 있습니다. ( Ensures your UI matches your data state )

LiveData 는 Observer Pattern을 따릅니다.  즉, LiveData 의 데이터가 변경될 때 마다 Observer 객체에게 알려줍니다.
Observer 객체에서는 LiveData의 데이터가 변경될 때 마다 UI 를 Update 하는 로직을 작성해둔다면
UI 와 데이터 상태의 일치를 보장할 수 있어집니다. 

 

2. 메모리 누수가 없습니다. ( No memory leaks )

메모리에 올라가있는 인스턴스를 앱이 더이상 필요하지 않음에도 GC 의 대상이 되지 못하면, 메모리누수가 발생합니다. 
LiveData 는 수명주기가 끝나면 자동으로 삭제됩니다. 즉, 메모리누수를 방지할 수 있습니다.

 

3. 중지된 액티비티로 인한 비정한 종료가 없습니다. ( No crashes due to stopped activities )

LiveData 를 관찰하고있는 Activity, Fragment, Services 등의 중지된 상태에서는 LiveData 의 변경사항에 대해 수신하지 않습니다.
따라서, LiveData 가 변경되었을 때 UI 를 수정하는 코드를 작성했다고 해도 동작하지 않으면서 비정상 종료에서 벗어날 수 있습니다.

 

4. 수동으로 수명 주기를 처리하지 않습니다.( No more manual lifecycle handling )

UI 를 구성하는 Component 들은 LiveData 를 관찰하기만 할뿐, 그 관찰을 중지하거나, 다시시작하는 행위를 하지 않습니다.
관찰하는 동안 LifeCycle 상태의 변경을 인식하기 때문에 이러한 사항들은 자동으로 관리합니다.

 

5. 항상 최신 데이터 유지합니다. ( Always up to date data )

만약 Oberver 객체가 백그라운드 상태가 되었을 때, 포그라운드 상태로 돌아오면, 그 즉시 LiveData 의 최신 데이터를 수신합니다.
따라서, 백그라운드 상태에 있을 때 데이터가 업데이트 되더라도, 포그라운드로 돌아오면 항상 최신 데이터를 유지합니다.

 

6. 구성(configuration)이 변경되었을 때, 적절하게 반응합니다. ( Proper configuration changes )

기기가 회전과 같은 구성(configuration) 의 변경으로 인한 Activity, Fragment 재생성등의 상황이 발생하면 사용가능한 LiveData 의 최신 데이터를  수신합니다.
따라서, 흔히 화면 회전시 사용해야 하는 Data 가 유실되는 경우를 쉽게 방지할 수 있어집니다.

 

7. 리소스를 공유합니다. ( Sharing resources )

LiveData 가 Observer 객체의 LifeCycle 을 인식할 수 있다는 것은, Activity, Fragment, Services 간에 객체를 공유할 수 있다는 것입니다.
LiveData 를 상속받아서 새로운 LiveData 클래스를 만들고, 싱글톤 패턴을 사용하여 구현할 수 있습니다.
<상세링크>

3. 오늘의 결론

LiveData 를 단순히 예제가 그러니까, 많은 코드들에서 쓰이고 있어서, 라는 이유로 사용하기 보다는

LiveData 의 장점들을 한번 더 기억하고, 그러한 장점을 살리는 코드를 작성해보는건 어떨까요?

'BackUp (관리중지) > Android 이론' 카테고리의 다른 글

Android Service  (0) 2021.05.14
Android Coroutine [코루틴]  (0) 2021.05.04
Fragment 와 Fragment LifeCycle 분석  (0) 2021.04.19
Activity 와 Activity LifeCycle 분석  (0) 2021.04.15
Android Context 에 대한 분석  (1) 2021.04.14

1. Fragment 란?

Fragment 는 FragmentActivity 내에서 동작이나 UI(부분) 을 나타냅니다.
( FragmentActivity 는 추후 학습, Activity 의 한 종류라고 생각하고 넘어가시고 나중에 정리해두겠습니다. (Activity, AppBarActivity, AppcompatActivity, FragmentActivity 등등...)

 

2. Fragment 특징

1. 하나의 Activity 내에서 여러개의 Fragment를 결합하여 window 가 여러개인 UI 를 빌드할 수 있다.

2. Fragment 는 여러개의 Activity에서 재사용이 가능하다.

3. Fragment 는 자체적인 수명주기를 가지고, 자체적인 입력 이벤트를 수신할 수 있다.

4. Fragment 는 Activity 실행중에 추가 및 삭제가 가능하다.

5. Fragment 는 항상 Activity 내에서 사용되어야 한다. 이 때, Fragment 수명주기는 Activity 의 수명주기에 직접적인 영향을 받는다.

6. Frgment 를 Activity 에서 사용하는 Layout 에 추가하는 경우 해당 Fragment 는 해당 Activity 의 ViewGruop 에 속하게 되고,

자체적인 View Layout 을 정의한다.

7. Fragment 는 Layout.xml 에서 <fragment> 요소로 선언하거나, 기존의 ViewGroup 에 추가하는 방법을 사용하면 Code 에서 Fragment 를 Layout 에 삽입이 가능하다.

 

3. Fragment LifeCycle

https://developer.android.com/guide/fragments/lifecycle

 

 

1. Fragment LifeCycle 가져오기 ( kotlin )

this.lifecycle.currentState

onCreateView 에서 해당 코드 출력결과

2. View LifeCycle 가져오기 ( kotlin )

this.viewLifecycleOwner.lifecycle.currentState

onCreateView 에서 해당 코드 출력결과

Fragme LifeCycle 단계 정리

Fragment LifeCycle Method View LifeCylce explanation
INITIALIZED onAttach NULL 프래그먼트가 액티비티와 연결되는 순간에 호출
CREATED onCreate NULL 프래그먼트를 처음 생성할 때 호출
CREATED onCreateView INITIALIZED 프래그먼트에 UI 를 그려야하는 순간에 호출하고, 
프래그먼트에 UI 를 그린다면 view 를 반환.
만약 프래그먼트가 UI 를 그리지 않는다면, null 을 반환
CREATED onActivityCreated INITIALIZED 프래그먼트생성이 완료되었다는 것을 액티비티에 알릴 때 호출
CREATED onViewStateRestored CREATED 프래그먼트에 저장된 상태를 복원했을 때 호출
STARTED onStart STARTED 프래그먼트가 화면에 보여지는 순간에 호출
RESUMED onResume RESUMED 프래그먼트가 사용자와 상호작용을 하는 순간에 호출
STARTED onPause STARTED 프래그먼트가 사용자와 상호작용을 하지 못하는 순간에 호출
CREATED onStop CREATED 프래그먼트가 화면에서 사라지는 순간에 호출
CREATED onSavedInstanceState CREATED 프래그먼트의 상태를 저장할 때 호출
CREATED onDestroyView DESTROYED 프래그먼트의 UI 가 사라져야 하는 순간에 호출
DESTROYED onDestroy NULL 프래그먼트가 제거될 때 호출
DESTROYED onDetach NULL 프래그먼트가 액티비티와 연결이 끊어질 때 호출

* LifeCycle 이 NULL 일때 State 를 가져오려고 하면 에러가 발생한다는 당연한 사실을 기억하세요

 

Activity LifeCycle 과 비교하면, 더 복잡한 LiceCycle 을 가지고 있지만, 완전히 전혀 다른 느낌의 구조는 아닙니다.

 

결국 우리는 Activity 내에서 Fragment 를 결합하여 사용하기 때문에 Fragment 는 사용된 Activity 의 LifeCycle 의 영향에서 벗어날 수가 없습니다.

 

그렇다면, Activity 에 Fragment 를 결합할 때 발생하는 전체적인 LifeCycle 은 어떻게 되는지 알아보겠습니다.

 

https://stackoverflow.com/posts/58029686/revisions

이 그림만 봐서는 와...뭐야 이거? 싶으실 수 도 있습니다.

Android Fragment 실습에서 해당 동작에 대해 직접적으로 Log 를 찍어보면서 확인하는 글을 작성하겠습니다. :)

 

4. Fragment 특징별 설명

1. 하나의 Activity 내에서 여러개의 Fragment를 결합하여 window 가 여러개인 UI 를 빌드할 수 있다.

- 스마트폰에서의 메모장을 생각해볼까요? 첫 화면에서 제목(TitleFragment)들을 보여주고, 해당 제목을 누르면 내용(MemoFragment)
을 보여준다고 해보겠습니다.

- 태블릿에서는 어떨까요? 좌측에 제목(TitleFragment)들을 보여주고, 우측에 내용(MemoFragment) 보여줄 수도 있습니다.

즉, 태블릿에서는 하나의 Activity 에 동시에 TitleFragment 와 MemoFragment . 즉, 여러개의 Fragment 가 결합되어 사용된다는 것을 알 수 있습니다.

 

2. Fragment 는 여러개의 Activity에서 재사용이 가능하다.

- 스마트폰에서 첫화면(MainActivity) 에서 TitleFragment 만 보여주었다고, 해당 제목을 클릭했을 시에 새로운 상세화면(DetailActivity) 에서 MemoFragment 만 보여줄 수 있습니다. 이렇게 여러 Activity 에서 필요로 하는 Fragment 만을 결합하여 사용할 수 있습니다.

좌측 : 스마트폰, 우측 : 태블릿

 

3. Fragment 는 자체적인 수명주기를 가지고, 자체적인 입력 이벤트를 수신할 수 있다.

- 3. Fragment LifeCycle 설명 참조

 

4. Fragment 는 Activity 실행중에 추가 및 삭제가 가능하다.

- 실습예정

 

5. Fragment 는 항상 Activity 내에서 사용되어야 한다. 이 때, Fragment 수명주기는 Activity 의 수명주기에 직접적인 영향을 받는다.

- 3. Fragment LifeCycle 설명 참조

 

6. Frgment 를 Activity 에서 사용하는 Layout 에 추가하는 경우 해당 Fragment 는 해당 Activity 의 ViewGruop 에 속하게 되고,

자체적인 View Layout 을 정의한다.

 

7. Fragment 는 Layout.xml 에서 <fragment> 요소로 선언하거나, 기존의 ViewGroup 에 추가하는 방법을 사용하면 Code 에서 Fragment 를 Layout 에 삽입이 가능하다.

- 실습예정

 

 

 

5. 오늘의 결론

1. 실습을 통해서 알아보겠지만, 가장 중요한것은 Fragment 의 LifeCycle 을 Activity LifeCycle 과 함께 신경써야 한다는 것입니다.

Fragment LifeCycle 만 생각한채로 진행하면, 그 과정도 결과도 쉽지 않을 수 있습니다.

 

2. 저는 1Activity n Fragment 를 지향하지 않습니다. 그렇다고 해서 n Activity 0 Fragment 를 지향하지도 않습니다.

때에 따라서 적절한 n 개의 Activity 와, 재사용성이 필요하고 고려되는 측면들에서 n 개의 Fragment 를 사용하는 등으로

적절하게 섞어주는게 좋을 것 같습니다.

 

만약, 하나의 DeviceSize 만을 지원하는 앱이면 n Activity 0 Fragment 로 해도 되나요? 라는 질문을 받는다면

저는 그 Device에서 지원하는 앱이 그 내부적으로 재사용 가능한 UI 가 없습니까? 를 묻고 싶습니다.


모든 Activity 로 Fragment 를 대체할 수 있고, 1Activity n Fragment 로도 n Activity 를 대체할 수 있습니다.

A가 좋으니 A만 쓴다.

B가 좋으니 B만 쓴다.

이런 생각보다는 A의 장점과 B의 장점을 고려하여 상황에 맞게 쓴다. 로 생각하시면 좋을 것 같습니다.


ps. 정말 극단적으로 A보다 B가 모든 측면에서 좋다면, A라는 기술을 사라질 것입니다. (그것이 라이브러리라면, 더이상 업데이트를 지원하지 않거나, 없애거나 하는 등의 결과가 나오겠죠?)
ex. butterknife 는 공식적으로 업데이트하지 않겠다고 선언한 라이브러리입니다. viewBinding 을 사용하라고 안내하고 있습니다.

 

 

작성하다보니까 깨달은 것은 실습 글을 빨리 작성해야겠네요.

 

developer.android.com/reference/android/app/Fragment

 

Fragment  |  Android 개발자  |  Android Developers

 

developer.android.com

 

'BackUp (관리중지) > Android 이론' 카테고리의 다른 글

Android Service  (0) 2021.05.14
Android Coroutine [코루틴]  (0) 2021.05.04
[Android Jetpack] LiveData란,  (0) 2021.04.21
Activity 와 Activity LifeCycle 분석  (0) 2021.04.15
Android Context 에 대한 분석  (1) 2021.04.14

1. Android System 은 LifeCycle(생명주기) 의 특정단계에 해당하는 특정한 콜백 메소드를 호출하여 Activity Instance 의 코드를 시작합니다.

- Activity Class 에는 Main 이 없다.

- Activity Class 에는 LifeCycle 에 해당하는 콜백 메소드가 존재한다.

- Activity Instance 에서 콜백 메소드는 Android System 이 호출한다.

 

2. Activity는 앱이 UI를 그리는 window를 제공합니다. 이 window는 일반적으로 Screen을 채우지만 (Full-Screen) Screen보다 작고 다른 window 위에 떠 있을 수 있습니다. 일반적으로 하나의 Activity 는 앱에서 하나의 Screen을 구현합니다.

- Activity 는 UI가 있는 App Component(앱 구성요소) 이다.

- Activity 는 window을 가득 채울수도 있고, 그보다 작은 상태로 window위에 있을 수 있다.

- 일반적으로는 Activity 는 앱에서 하나의 Screen을 구현한다.
* widnow 와 screen 은 다릅니다.

3. Activity LifeCycle

 

https://developer.android.com/reference/android/app/Activity

 

Activity 는 사진과 같은 LifeCycle 을 갖는다. 

 

onCreate()

Android System 에서 Activity 를 Instance화하기 위하여 호출하는 메소드

 

이 메소드에서는 setContentView() 를 이용하여 UI Layout 을 정의할 수 있습니다. 

* setContentView() 를 사용하지 않으면 어떻게 되나요? UI Layout이 없으니까 에러인가요?

 

setContentView() 를 작성하지 않으면, UI Layout 이 정의되지 않은 window 위에 빈 screen이 생성됩니다.
Activity 는 Screen보다 작은 크기로 다른 window 위에 존재할 수 있다 역시 성립합니다.

onStart()

Screen 이 보여지는 순간에 실행됩니다.

 

어라? setContentView() 가 Screen 의 UI Layout 을 정의하는 거라면, 어짜피 보여질 때 onStart() 가 호출되니까 이때 setContentView() 를 사용해도 되지 않아요?

 

setContentView() 를 onStart() 에서 호출한 경우

가능은 합니다. 그런데, 왜? setContentView() 를 onCreate() 에 쓰는걸까?

Docs 에 의하면,

onCreate()
활동이 처음 생성 될 때 호출됩니다.
 여기에서 뷰 생성, 목록에 데이터 바인딩 등 일반적인 정적 설정을 모두 수행해야합니다.이 메서드는 활동의 이전에 고정 된 상태 (있는 경우)를 포함하는 번들도 제공합니다.
라고 되어있습니다.

음? 으음? 싶으시다면, 한가지 예를 들어보겠습니다.

Bundle 에는 Activity Instance 의 상태들을 저장할 수 있습니다. ( 변수를 저장할수도 있습니다. ) 이때 Bundle 에 저장된 상태를 이용하여 layout 을 수정한다고 생각하면,

우리는 onStart() 에서 Bundle 에 접근하기 위한 대책을 추가해야합니다. (변수를 추가로 넣는 등)

savedInstanceState 가 null 이므로, USER 에 해당하는 activity_next 를 layout UI 로 정의

또한 꼭 이 때문은 아니더라도, 문서에서 제공하는 onCreate() 의 정의에 맞게 사용하는게 옳바른 코드의 작성방법 이라고 할 수 있겠죠?

 

onResume()

이 메소드는 사용자와 상호작용을 하는 순간에 실행됩니다.

즉, 사용자의 터치이벤트, 키보드로 입력넣기, 핸드폰을 흔들 때 반응해야하기 등등은

이 메소드가 호출되어야 가능합니다.

 

Activity Running

만약 우리가 C,Java 등의 언어에서 main() 을 실행하고 종료되기 전까지는 Running 단계겠죠?

마찬가지로 onResume() 이 실행된 이후부터는 Running 단계입니다.

 

onPause()

이 메소드는 사용자와 상호작용을 하지 못하는 순간에 호출됩니다.

그렇다고, 화면이 사라지는 상태를 말하는 것은 아닙니다.

또한, 사용자와 상호작용을 하지 못할뿐, 화면에 사라진 상태는 아니기 때문에 UI 를 업데이트 할 수 있습니다.

MainActivity 의 상호작용은 멈추었지만, UI 업데이트중

* 위와같은 상황에서 다시 MainActivity 로 돌아간다면, MainActivity 가 사용자와 상호작용이 가능해지니까 onResume() 이 실행됩니다.

 

onStop()

이 메소드는 더이상 Activity 의 Screen 이, 사용자에게 보여지지 않는 순간에 실행됩니다.

하지만, Instance 가 없어진 상태는 아닙니다. 

* 특정한 조건없이 1초마다 count를 증가하는 Thread 가 실행중이였다면, onStop() 이후에도 Thread는 멈추지 않고 count를 계속 증가시깁니다. 때에 따라서는 필요할수도, 굉장히 위험할수도 있습니다.

(불필요한 코드가 계속 돌고 있다는 것은 성능저하까지 이어질 수 있...)

onStop() 이후에 굳이 필요없는 코드는 잠시 멈춰두는 것을 추천합니다. 밑에 나오는 onRestart() 에서 다시 실행시키면 되죠!

 

 

onRestart()

onStop() 메소드가 실행된 이후에, Activity 의 Screen 이, 다시 사용자에게 보여지는 순간에 호출됩니다.

즉, onStop() 이후에 screen이 보여진다면 onStop() -> onRestart() -> onStart() 순으로 진행되고,

onCreate() 이후에 screen이 보여진다면 onCreate() -> onStart() 순으로 진행됩니다.

* onPause() 에서 onResume() 이 호출되고, onRestart()도 결국 onRestart() -> onStart() -> onResume() 이 호출되면서

onResume() 에 onStop() 이후에 얻어지는 데이터를 넣는 경우가 있습니다.

저는 onResume() 에서는 onResume() 상황에 맞는 코드.

onRestart() 에서는 onRestart() 상황에 맞는 코드 만을 작성하는 것을 추천합니다.

 

onDestroy()

이 메소드는 Activity 가 완전히 사라지는 순간에 호출됩니다. 문서에 의하면, 현재 Activity Instance 에서 finish 를 사용하는 경우에 호출되거나, Android System 에서 메모리절약을 위해 일시적으로 파괴하면서 호출이 된다고 하는데, 이는 isFinishing() 으로 구분이 가능하다고 작성되어 있습니다.

 

만약 finish 를 사용하거나, Back 키를 사용하는 등으로 Activity 가 사용자에 의해 파괴되는 경우에는 isFinishing() 은 true 를 반환합니다.

Android System 에서 메모리절약을 위해 파괴하는 거라면 false 를 호출합니다.

* onDestroy() 에서 isFinishing() 가 true 일 때 데이터를 저장하거나 하는 행위는 위험합니다.

( 쉽게 onDestroy() 에서 데이터를 저장하지 마세요. )

*Android System 은 Process 만을 kill 합니다. 

*만약, Process 가 제거되는 순간을 경험하고 싶다면, 개발자옵션 -> 프로세스수제한 -> 개수선택

이후 다른 앱들을 실행시켜보세요. 개수가 가득차면 Process 가 제거되면서  false 를 리턴합니다.

 

 

 

4. 정리

1. Activity 는 Android System 가 콜백메소드(onCreate)를 호출하여 생성하며 콜백메소드(Create~Destroy)는 LifeCycle 에 맞게 호출된다.

 

2. Activity 는 UI Layout을 정의하여 screen에 보여줄 수 있고, 만약 UI Layout 을 정의하지 않는다면 빈 screen이 보여진다. 또한 UI 가 없기 때문에 사용자에 맞춤 기능이 아니라, LifeCycle 에 해당하는 작성된 코드들만이 실행된다.

 

3. 일반적으로 1개의 Activity 는 1개의 Screen 을 구현한다.

 

4. Activity 의 window 와 screen 은 다르다.

 

5. LifeCycle 은 onCreate() , onStart(), onRestart(), onResume(), onPause(), onStop(), onDestroy()로 이루어져있다.

onCreate() : Android System 에서 Actvitiy 를 Instance화 하는 순간

onStart() : Screen 이 보여지는 순간

onRestart() : onStop() 이후에 Screen 이 보여지려고 하기(onStart()) 직전

onResume() : 사용자와 상호작용이 일어나기 시작하는 순간

onPause() : 사용자와 상호작용이 끊어지는 순간

onStop() : Screen 이 보여지지 않는 순간 : 단 1pixcel 이라도 Screen 이 보여진다면, onStop() 에 진입하지 않는다.

onDestroy() : Code(finish) or 사용자에 의해(back or finish) or AndroidSystem 이 Process 를 kill 하는 순간 - isFinishing()으로 구분

 

5. 오늘의 결론

생명주기에 맞게 코드를 작성하고, Android 문서는 영어로 보거나, 영어와 한글을 같이 보자.
( Window 랑 Screen 을 둘다 "화면" 이라고 번역시켜놔서 뇌에 혼란이 엄청왔다. )

PS.

일반적으로 1Activity 는 1개의 Screen 을 구현한다. 라고 되어있는 부분에서

1Activity 에서 어떤 경우에 2개 이상의 Screen 을 구현하는지는 좀더 찾아봐야 할 것 같습니다.


여러 의견 모음 (2021.04.15 기준)

1. Fragment 를 이용하여 화면에 보여주는 View 가 다른데, 이런 경우에 2개 이상의 Screen 을 구현하는 것 같다.
- 저는 Activity 가 Window 에 Screen 을 보여주고, 이때 Screen 은 setContentView() 를 사용하여 정의한다고 되어 있는 부분에서 이 부분이 의아했습니다. Fragment 에 대해서는 추가 글에서 정리하겠지만, Fragment 는 Activity 이후에 나온 기술로, Activity 에 대해 설명하는 기준에 있어서 Fragment 를 사용하는 경우를 2개 이상의 Screen 을 구현했다고 생각하지 않습니다.

뭔가 근본적으로 Screen 에 대한 정의가 확실히 잡혀있지 않은 상태여서 그 부분을 찾아보고 있지만, Fragment 가 나오기 전부터 Screen 을 2개 이상 구현할 수 있었고, 그 로직의 단점을 보완하고자 Fragment 가 등장한것이 아닐까 라는 생각을 합니다.

 

이 부분에 대해 정확하게 설명이 가능한 블로그 or 지식을 보유중이신 분이 있으시다면 공유부탁드리겠습니다..

'BackUp (관리중지) > Android 이론' 카테고리의 다른 글

Android Service  (0) 2021.05.14
Android Coroutine [코루틴]  (0) 2021.05.04
[Android Jetpack] LiveData란,  (0) 2021.04.21
Fragment 와 Fragment LifeCycle 분석  (0) 2021.04.19
Android Context 에 대한 분석  (1) 2021.04.14

Android 앱을 "한 번" 이라도 해본 사람은 Context 를 보지 못했을 수가 없습니다.

저는 Context 를 사용할 때, Context 가 대체 뭐야? 하고 상위클래스를 따라가보다가 수천줄에 기겁하고, 닫아버렸던 기억이 있네요.

그렇게 얕게 Context 가 무엇인지 대충 블로그를 통해 학습하고, 넘겼었습니다.

 

이번엔 그런식으로 넘기는게 아니라, 다시 한 번, Context 에 대하여 분석을 해볼까 합니다.

 

Android Reference 에 나온 설명
Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.

애플리케이션 환경에 대한 글로벌 정보에 대한 인터페이스입니다. Android 시스템에서 구현을 제공하는 추상 클래스입니다. 이를 통해 애플리케이션 별 리소스 및 클래스에 액세스 할 수있을뿐만 아니라 활동 시작, 인 텐트 브로드 캐스트 및 수신 등과 같은 애플리케이션 수준 작업에 대한 상향 호출도 가능합니다.

응 블로그 열면 거의 인삿말이이였지, 그래서?


1. Context 는 글로벌 정보에 대한 인터페이스입니다. (Context 는 추상클래스입니다.)

2. Context 는 Android 시스템에서 구현을 제공합니다. 

 

일단 이 2가지에 대한 설명을 기억한채로 이어가겠습니다.

 

 

 

 

Context를 직접적으로 상속받는 하위 클래스와 간접적으로 상속받는 하위 클래스 (간접으로 상속받는 하위 클래스는 더 많습니다.)

 

일단, 우리에게 익숙한 클래스인 Application 과 Activity, 그리고 Service 를 보였습니다.

더보기

Android Component

  • Activity  : Context 의 하위 클래스
  • Service : Context 의 하위 클래스
  • Broadcast receiver : public abstract class BroadcastReceiver { ... }
  • Content provider : public abstract class  ContentProvider implements ContentInterface, ComponentCallbacks2 {...}

 

위에 작성한 것처럼, Android Component 중에서는 Activity 와 Service 만 Context 의 하위 클래스입니다.

 

아, 그렇다면 Context 가 최상위에 존재하고, Application , Activity, Service 들은 하위 클래스 이므로, 서로 다른 Context 가 생성되겠구나!

까지만 생각하지 마시고, 우리는 좀더 깊게!!

 

Application 의 Context 는 Singleton 입니다.

즉, 1개의 앱은 1개의 Application Context 만을 갖고 있습니다.

Activity 와 Service 의 Context 는 생성될 때마다 각자의 새로운 Context 를 갖고 있습니다.

 

이해를 돕기 위하여 바로 다음단계를 작성하겠습니다.

LifeCycle 에 따른 사용 가능한 Context

Application Context 와 Activity Context 의 존재시점

위 그림처럼 Context 는 LifeCycle 과 함께 존재합니다.

자, 이제 우리는 getApplicationContext(), getBaseContext(), this 등으 로 아무 생각없이 사용하던 Context 가 어느 곳에 있는 Context 를 사용하고 있는 것인지 생각할 수 있게 되었습니다.

 

Context 를 사용하는 코드로 이 글의 결론에 한발짝 다가서봅니다.

1. Activity 에 선언된 View 의 Context 는 누구일까?

public class MainActivity extends Activity {
    private final String TAG = MainActivity.class.getSimpleName();
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.tvTitle);

        if (textView.getContext() == getApplicationContext()) {
            Log.d(TAG,"Application Context");
        }

        if (textView.getContext() == this) {
            Log.d(TAG,"Activity Context");
        }
    }
}

정답은 Activity Context 입니다.

위 코드처럼 작성한 View 는 Activity Context 를 전달받게 됩니다. 따라서 View 의 Context 는 Activity Context 와 동일합니다.

 

2. CustomView 를 만들어서 xml 에 직접 추가하는 경험을 하신 분들도 계실겁니다. 그렇다면 View 를 직접 만들 땐..?

public class MainActivity extends Activity {
    private final String TAG = MainActivity.class.getSimpleName();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView1 = new TextView(this);
        TextView textView2 = new TextView(getApplicationContext());

        if (textView1.getContext() == getApplicationContext()) {
            Log.d(TAG,"textView1 : Application Context");
        }

        if (textView1.getContext() == this) {
            Log.d(TAG,"textView1 : Activity Context");
        }

        if (textView2.getContext() == getApplicationContext()) {
            Log.d(TAG,"textView2 : Application Context");
        }

        if (textView2.getContext() == this) {
            Log.d(TAG,"textView2 : Activity Context");
        }
    }
}

전달받은 Context 가 View 의 Context 가 됩니다.

이 상황에서 문제가 발생합니다.

현재 코드의 이상향은, MainActivity 가 종료되고 다른 Activity 가 실행된다고 하면, MainActivity 에 있는 객체들은 GC 의 대상이 되어야 합니다.

그러나 textView2 같은 경우는 Application Context 를 사용하여 만들어졌고, Application Context 를 참조하고 있습니다.(실행결과)

 

그렇다면 textView2 는 GC 대상에서 제외됩니다. 이런 경우 메모리 릭이 발생합니다..

*메모리 릭 : 메모리 누수(memory leak) 현상은 컴퓨터 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상이다. (wiki)

 

유사 케이스로 아래처럼 View 를 static 으로 선언하여 사용하려는 경우에도 메모리 릭이 발생합니다.

(하지만, 이 경우는 경고 메시지를 통해서 '아 static 쓰지 말라는구나' 하고 지워버리기 때문에 그냥 넘어가는 분들도 계실겁니다.)

 

3. View 만 조심하면 되는데, 그럼 끝난거 아닌가?

 

public class MyClass {
    private static MyClass instance;
    private Context mContext;

    public static MyClass getInstance(Context context) {
        if(instance == null) {
            instance = new MyClass(context);
        }
        return instance;
    }

    private MyClass(Context context) {
        mContext = context;
    }

    public String toString(Context context) {
        StringBuilder sb = new StringBuilder();
        sb.append("MyClass: ");
        if(context == mContext) {
            sb.append("True");
        } else {
            sb.append("false");
        }
        return sb.toString();
    }
}

위와 같이 context 를 전달받는 싱글톤 객체가 있다고 가정하겠습니다.

 

public class MainActivity extends Activity {
    private final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d(TAG, MyClass.getInstance(this).toString(this));

        startActivity(new Intent(MainActivity.this, NextActivity.class));
        finish();
    }
}

public class NextActivity extends Activity {
    private final String TAG = NextActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d(TAG, MyClass.getInstance(this).toString(this));
    }
}

그리고 이렇게 실행한 결과를 보도록 하겠습니다.

MainActivity 에서는 MainActivity 의 Context 와 싱글토 객체의 Context 가 일치합니다.

MainActivity 의 Context 를 전달해 주었기 때문이죠..

 

그러나 NextActivity 에서는 false 가 출력됩니다.

NextActivity 의 Context 는 MainActivity 의 Context 와 다르기 때문이죠.

 

그럼 Application Context 랑 비교하면?

저희는 위에서 공부했듯이 당연히 false , false 가 출력될 것이라는 것을 알 수 있습니다.

 

그렇다면, 이 코드의 문제는?

역시나, MainActivity 의 Context 가 사라지지 않는다는 것입니다.

싱글톤 객체는 어짜피 유지되어야 하지 않나요?

우리는 싱글톤 객체가 유지되기를 원한것이지, MainActivity의 Context 까지 유지되기를 원한 것이 아닙니다.

만약 싱글톤 객체에서 Context 의 기능이 필요하다면 Application Context 를 사용하는게 메모리릭을 방지 할 수 있겠죠?

Application Context 는 앱에서 단 한개만 존재하고, 그렇다는 것은 어디서 비교해서 true true 가 나오겠죠?

 

 

오늘의 결론

1. Application Context 는 실행이후 단 1개만 존재하며, Application 의 LifeCycle 과 함께 해야 한다. 

2. Activity Context 는 생성되는 경우에 따라 여러개 존재하며, Activity 의 LifeCycle 과 함께 해야 한다.

3. 만약 Activity LifeCycle 과 함께하는 데이터가 Application Context 를 사용한다면, 그것은 곧 메모리 릭으로 이어진다.

 

* Context 사용시 LifeCycle 을 고려하여 작성하자! 
이 객체는 Activity 에서 사용하니까 Activity Context 를 써야지 라고 생각하면 안된다!!

객체의 LifeCycle 을 고려해야 한다!! Static 변수를 조심하자!!

메모리 릭이 계속 발생한다면, 정상적으로 동작하던 앱을 끄지 않고 이것저것 사용하는 도중에 갑자기 꺼질 수도...

'BackUp (관리중지) > Android 이론' 카테고리의 다른 글

Android Service  (0) 2021.05.14
Android Coroutine [코루틴]  (0) 2021.05.04
[Android Jetpack] LiveData란,  (0) 2021.04.21
Fragment 와 Fragment LifeCycle 분석  (0) 2021.04.19
Activity 와 Activity LifeCycle 분석  (0) 2021.04.15

+ Recent posts