본문 바로가기

책 요약하기/이것이 안드로이드다

#5-1. 화면 구성하기 2021-03-12

1. 컨텍스트

1.1 컨텍스트란

컨텍스트(Context)는 시스템을 사용하기 위한 정보(프로퍼티)와 도구(메서드)가 담겨 있는 클래스입니다. 대부분의 컨텍스트는 컴포넌트 실행(Running) 시 함께 생성되고, 생성된 컴포넌트가 가지고 있는 메서드를 호출해서 각각의 도구들을 사용할 수 있습니다. 안드로이드에서의 컴텍스트는 입을 실행하기 위해 잘 짜여ㄷ진 설계도의 개념으로 앱에서 사용하는 기본 기능이 담겨있는 기본 클래스(Base Class)입니다. 액티비티는 컨텍스트를 상속받아 구현됩니다. 액티비티처럼 컨텍스트를 상속받은 컴포넌트들은 코드상에서 baseContext를 호출하는 것만으로 안드로이드의 기본 기능을 사용할 수 있습니다.

 

컨텍스트의 종류

1. 애플리케이션 컨텍스트(Application Context)

애플리케이션과 관련된 핵심 기능을 담고 있는 클래스입니다. 앱을 통틀어서 하나의 인스턴스만 생성됩니다. 액티비티나 서비스 같은 컴포넌트에서 applicationContext를 직접 호출해서 사용할 수 있는데 호출하는 지점과 관계없이 모두 동일한 컨텍스트가 호출됩니다.

2. 베이스 컨텍스트(Base Context)

안드로이드의 4대 메이저 컴포넌트인 액티비티, 서비스, 컨텐트 프로바이더, 브로드캐스트 리시버의 기반 클래스입니다. 각각의 컴포넌트에서 baseContext 또는 this로 컴텍스트를 사용할 수 있고 컴포넌트의 개수만큼 컨텍스트도 함께 생성되기 때문에 호출되는 지점에 따라 서로 다른 컨텍스트가 호출됩니다.

 

컴포넌트별 컨텍스트의 기능

각각의 컴포넌트의 컨텍스트에서 지원하는 기능입니다.

 

1.2 인텐트

액티비티를 실행하기 위해서는 단순히 컨텍스트가 제공하는 메서드를 호출하면 되는데, 이때 실행할 액티비티가 명시된 인텐트(Intent)를 해당 메서드에 전달해야 합니다. 인텐트는 개발자가 어떤 의도를 가지고 메서드를 실행할 것인지를 인텐트에 담아서 안드로이드에 전달하면 안드로이드는 해당 인텐트를 해석하고 실행합니다.
액티비티를 실행하려면 기본적으로 인텐트가 필요하지만, 프로젝트를 생성할 때 함께 만들어지는 MainActivity는 특별한 설정을 하지 않아도 안드로이드에 자동으로 등록되고 실행됩니다. 하지만 MainActivity 외에 다른 액티비티를 사용할 떄는 인텐트에 새 액티비티의 이름을 담아서 시스템에 전달합니다.

  1. 실행할 대상의 액티비티 이름과 전달할 데이터를 담아서 인텐트를 생성합니다.
  2. 생성한 인텐트를 startActivity() 메서드에 담아서 호출하면 액티비티 매니저에 전달됩니다.
  3. 액티비티 매니저는 인텐트를 분석해서 지정한 액티비티를 실행시킵니다.
  4. 전달된 인텐트는 최종 목적지인 타깃 액티비티까지 전달됩니다.
  5. 타깃 액티비티에서는 전달받은 인텐트에 데이터가 있다면 이를 꺼내서 사용할 수 있습니다.

 

메인 액티비티에서 서브 액티비티 실행하기

Intent를 사용하기 위한 작성규칙입니다. (SubActivity 코틀린 파일과 activity_sub.xml파일을 생성하고 AndroidManifest.xml파일을 수정해야 합니다.)

val intent = Intent(this, SubActivity::class.java)
btnStart.setOnClickListener { startActivity(intent) }

 

1.4 액티비티 사이에 값 주고받기

액티비티와 같은 컴포넌트는 인텐트에 실행 메시지도 전달하지만 인텐트를 통해 데이터도 주고받을 수 있습니다. 인텐트 내부에는 번들(bundle)이라는 데이터 저장 공간이 있는데, 이 번들에 데이터를 담아서 주고받을 수 있습니다. 
인텐트에 값을 입력할 때는 키와 값의 조합으로 번들에 직접 넣고, 꺼낼 때는 처음 입력했던 키로 꺼냅니다.

var intent = Intent(this, SubActivity::class.java)
// putExtra()메서드로 데이터 전달
intent.putExtra("from1", "hello bundle")
intent.putExtra("from2", 2020)

// getExtra()메서드로 데이터 꺼냄
to1.text = intent.getStringExtra("from1")
to2.text = "${intent.getIntExtra("from2", 0)}"
intent는 익티비티의 기본 속성
intent가 액티비티의 기본 속성이기 때문에 전달된 인텐트는 intent로 바로 호출해서 사용할 수 있습니다.
getIntExtra의 두 번째 파라미터는 기본값
getIntExtra() 함수에 입력되는 두 번째 값은 from2 키로 값을 꺼냈는데 아무런 값도 전달되지 않았을 경우 디폴트로 사용할 기본값을 설정하는 파라미터입니다. 특별한 경우가 아니라면 '0'으로 입력하고 사용합니다.

 

메인 액티비티에서 값 돌려받기

이번에는 반대로 서브 액티비티가 종료되면 메인 액티비티로 값을 돌려주는 코드는 다음과 같습니다.

// SubActivity.kt에 작성 버튼 클릭시 현재 Activity를 닫고 MainActivity로 돌아오게 함

btnClose.setOnClickListener {
    val returnIntent = Intent()
    returnIntent.putExtra("returnValue", editMessage.text.toString())
    
    // 1번째 파라미터는 상태값, 2번째는 전달하려는 인텐트
    setResult(Activity.RESULT_OK, returnIntent)
    finish()
}
onActivityResult() 메서드의 구조
requestCode: 호출 시에 메인 액티비티에서 입력하는 코드 startActivityFroResult 메서드에 인텐트와 함께 입력해서 호출한 코드(또는 버튼)를 구분합니다.
resultCode: 결과 처리 후 서브 액티비티에서 입력하는 코드, 앞에서 RESULT_OK를 담아서 보냈습니다.
data: 결과 처리 후 서브 액티비티가 넘겨주는 인텐트가 담겨 있습니다.
// MainActivity.kt 작성
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    
    if (resultCode == Activity.RESULT_OK) {
    	val message = data?.getStringExtra("returnValue")
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
}

여기까지 작성하고 에뮬레이터에서 실행한 후 서브 액티비티에서 닫기버튼을 클릭해보면 어떠한 반응도 없는데 이유는 startActivity()로 실행된 액티비티에서는 값을 돌려받을 수 없기 때문입니다. 이런경우 startActivityForResult()메서드를 사용해야만 합니다.

// 두번째 파라미터는 메인 액티비티에서 서브 액티비티를 호출하는 버튼이 여러 개 있을 때 어떤 버튼에서 호출된 것인지를 구분하는 용도입니다.
btnStart.setOnClickListener { startActivityForResult(intent, 99)
...

btnStart.setOnClickListener { startActivityForResult(intent, 99) }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
	super.onActivityResult(requestCode, resultCode, data)
    
    // startActivityForResult() 메소드 호출 시 두번째 파라미터 번호를 99로 했기 때문에 요청 코드를 체크해서 
    // 해당 버튼을 클릭해서 얻은 결과를 받아 처리합니다.
    if (resultCode == Activity.RESULT_OK) {
    	when(requestCode) {
        	99 -> {
            	val message = data?.getStringExtra("returnValue")
                Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
            }
        }
    }
}

 

1.5 액티비티 생명주기

액티비티의 상태 변화에 따라 호출되는 생명 주기는 다음과 같습니다.

 

생명 주기 콜백의 이해

1. 먼저 액티비티를 생성하면 액티비티는 onCreate() - onStart() - onResume() 메서드가 호출됩니다. 여기서 종료하면 onPause() - onStop() - onDestroy() 메서드가 호출됩니다.

2. 만약 메인 액티비티에서 새로운 액티비티(완전히 메인 액티비티가 보이지 않는 상태)를 호출하게 되면 onPause() - onStop() 메서드 호출 후에 onCreate - onStart() - onResume()메서드가 호출되어 새로운 액티비티는 실행 상태가 됩니다. (투명한 즉, 완전히 메인 액티비티가 보이지 않는 상태가 아니라면 onPause()메서드만 호출됩니다. 여기서 투명하다는 의미는 스타일 속성이 투명하자는 의미입니다. :))

3. 이후 다시 메인 액티비티로 돌아오면 onPause() - onResume() - onStop() - onDestroy() 메서드 호출 후에 메인액티비티는 실행 상태가 됩니다. (onStop(), onDestroy() 메서드는 서브 액티비티가 종료되었기 때문에 호출됩니다.)

 

액티비티 백스택

백스택(Back Stack)은 액티비티 또는 화면 컴포넌트를 담는 안드로이드의 저장 공간입니다. 액티비티 A에서 액티비티 B를 실행하고, 다시 액티비티 B에서 액티비티 C를 실행하면 A-B-C순으로 (화면)백스택이 쌓이게 되고 사용자는 가장 위에 있는 액티비티(C)를 보게됩니다.

 

테스크와 프로세스

테스크(Task)는 애플리케이션에서 실행되는 프로세스(Process)를 관리하는 작업 단위입니다. 안드로이드는 애플리케이션의 실행 단위로 프로세스를 사용하는데 하나의 앱을 만들고 실행하면 앱당 하나의 프로세스가 생성되고 액티비티를 처리합니다.
안드로이드에서 테스크는 다른 프로세스의 액티비티를 함께 담을 수 있습니다. 안드로이드는 서로 다른 애플리케이션의 액티비티를 공유할 수 있는데 카메라와 갤러리 액티비티를 예로 들 수 있습니다. 카메라 기능을 간단한 코드로 호출해서 사용하면 실제로는 카메라 앱의 독자적인 프로세스가 실행되고 카메라 액티비티 또한 카메라 앱의 프로세스에 의해 처리됩니다.

카메라 기능도 하나의 앱
짧은 코드로 호출해서 사용하는 카메라도 하나의 앱으로 안드로이드에 미리 만들어져 있습니다. 카메라를 호출한다는 것은 카메라의 액티비티 이름을 담은 인텐트를 안드로이드에 전달하는 것입니다.

카메라를 사용하기 위한 인텐트를 시스템으로 전달하면 카메라 액티비티가 다른 앱(카메라도 하나의 독립적인 앱)에 있기 때문에 프로세스를 새로 생성합니다. 호출된 카메라 액티비티가 새로운 프로세스를 통해 동작하지만 하나의 작업 단위인 테스크로 묶입니다. 또한 마치 하나의 앱처럼 동일한 테스크로 묶이고 백스택에 쌓이게 됩니다.

같은 테스크의 백스택에 쌓이기 때문에 뒤로가기 버튼을 누르면 같은 앱의 액티비티처럼 백스택에서 제거되고, 홈 버튼을 누르면 마치 하나의 앱처럼 전체가 백그라운드로 이동합니다.

 

액티비티 테스크 관리하기

액티비티 태스크는 두 가지 방법으로 관리할 수 있습니다. 먼저 매니페스트 설정으로 관리하는 방법입니다. 태스크와 백스택으로 관리하는 액티비티는 설정 파일인 AndroidManifest.xml에 작성되는 <activity> 태그 안에 속성으로 다음 코드처럼 사용할 수 있습니다.

<activity android:name=".MainActivity" android:launchMode="singleInstance"></activiyu>
속성 설명
taskAffinity 기본값은 manifest에 정의된 패키지명으로 기본적으로 한 앱의 모든 액티비티는 동일한 affinity를 가집니다. allowTaskReparenting의 값에 따라 액티비티가 쌓일 태스크 스택을 결정할 수 있습니다. 입력값은 패키지명과 같은 형태입니다.
launchMode 호출할 액티비티를 새로 생성할 것인지 재사용할 것인지를 결정합니다. 기본값은 항상 새로 생성하도록 되어 있습니다. 네 가지 모드: standard, singleTop, singleTask, singleInstance
allowTaskReparenting 호출한 액티비티를 동일한 affinity를 가진 태스크에 쌓이도록 합니다.
clearTaskOnLaunch true면 액티비티가 재실행될 때 실행된 액티비티의 수와 관계없이 메인 액티비티를 제외하고 모두 제거합니다. 기본값은 false입니다.
alwaysRetainTaskState 기본 설정값이 false면 사용자가 특정 시간 동안 앱을사용하지 않을 경우 시스템이 메인 액티비티를 제외한 액티비티들을 제거합니다. true일 경우는 시스템이 관여하지 않습니다.
finishOnTaskLaunch 앱을 다시 사용할 때 기존 액티비티를 종료할지 여부를 결정합니다. 기본값이 false일 경우 종료하지 않습니다.

액티비티 태스크를 관리하는 또 다른 방법으로는 소스 코드에서 startActivity( ) 메서드에 전달하는 플래그 값으로 태스크를 관리하는 방법입니다. 

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

 

많이 사용하는 플래그 표 입니다.

플래그 설명
FLAG_ACTIVITY_CLEAR_TOP 호출하는 액티비티가 스택에 있으면 해당 액티비티를 Top으로 이동시키기 위해 그 위에 존재하던 액티비티를 모두 삭제합니다. 예를 들어 액티비티 A/B/C/D가 스택에 있을 때 C를 호출하면 D/E를 삭제해서 C를 화면에 나타냅니다.
FLAG_ACTIVITY_MULTIPLE_TASK 호출되는 액티비티를 메인으로 하는 새로운 태스크를 생성합니다. 이렇게 하면 동일한 액티비티를 하나 이상의 태스크에서 열 수 있습니다.
FLAG_ACTIVITY_NEW_TASK 새로운 태스크를 생성하여 생성된 태스크 안에 액티비티를 추가할 때 사용합니다. 단, 기존에 존재하는 태스크 중에 생성하려는 액티비티와 동일한 affinity를 가지고 있는 태스크가 있으면 해당 태스크로 액티비티가 들어갑니다. 하나의 애플리케이션 안에서는 모든 액티비티의 기본 affinity가 같은 태스크 안에서 동작하지만, 무조건 태스크가 새로 생성되는 것은 아니고 FLAG_ACTIVITY_MULTIPLE_TASK플래그와 함께 사용해야 합니다.
FLAG_ACTIVITY_SINGLE_TOP 호출되는 액티비티가 Top에 있으면 해당 액티비티를 다시 생성하지 않고, 존재하던 액티비티를 다시 사용합니다. 액티비티 A/B/C가 있을 때 C를 호출하면 기존과 동일하기 A/B/C가 나옵니다.

 

- 미니 퀴즈 5-1 -

1. 액티비티는 직접 생성할 수 없고 메시지를 안드로이드에 전달해서만 생성할 수 있습니다. 이 메시지는 무엇인가요?

답 : Intent(인텐트)

 

2. 안드로이드의 4대 핵심 컴포넌트 중에 화면을 담당하는 컴포넌트로 자신만의 생명 주기를 가진 이것은 무엇인가요?

답 : Activity(액티비티)

 

3. 앱을 실행한 상태에서 화면을 끄면 앱의 액티비티의 생명 주기는 어떻게 바뀌나요?

답 : 화면을 끄면 onPause() - onStop() - onDestroy() 메서드가 호출됩니다.

 

4. 액티비티 A에서 액티비티 B를 호출한 다음에 액티비티 B가 종료될 때 값을 돌려받으려면 액티비티 A에서는 어떤 메서드를 사용해서 액티비티 B를 호출해야 하나요?

: startActivityForResult(Intent, requestCode) 메서드를 호출해야 합니다.