[Android Fundamentals] Overview (2)
Manifest
안드로이드에는 앱의 진입점 역할을 하는 4개의 주요 컴포넌트가 있다. 그런데 이 진입점들이 실제로 시스템과 상호작용하기 위해서는 시스템에게 컴포넌트의 존재를 알려야 한다. 그 역할을 하는 것이 바로 ``AndroidManifest.xml``이다.
Manifest는 컴포넌트를 정의하는 역할 외에도 여러 기능을 수행한다.
- 앱에 필요한 사용자 권한을 정의한다. (인터넷, 연락처 접근 등)
- 앱이 설치될 수 있는 API 최소 레벨을 정의한다. 다만 API 레벨은 manifest보다는 ``build.gradle`` 파일에서 선언하는 경우가 대다수이다.
- 앱이 사용하는 하드웨어/소프트웨어 기능을 정의한다. (카메라, 블루투스 등)
주로 앱의 정적인 속성을 정의한다고 볼 수 있다. 권한이나 기능 등...
Manifest에 컴포넌트 정의
하지만 manifest의 가장 중요한 역할은 컴포넌트를 정의하는 것이다. 예를 들어 다음과 같이 activity를 정의할 수 있다.
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:icon="@drawable/app_icon.png" ... >
<activity android:name="com.example.project.ExampleActivity"
android:label="@string/example_label" ... >
</activity>
...
</application>
</manifest>
``<application>``에서는 앱 아이콘, 이름 등 앱의 정적인 속성을 지정할 수 있다. 그런데 이 속성들은 api 레벨처럼 ``build.gradle``에서 지정하는 경우도 많다.
``<activity>``에서는 activity의 패키지 경로를 ``android:name`` 속성으로 지정하고, ``android:label`` 속성을 통해 사용자에게 보일 수 있는 라벨을 지정한다.
컴포넌트를 정의하는 방법은 다음과 같다.
- Activity: ``<activity>``
- Service: ``<service>``
- Broadcast receiver: ``<receiver>``
- Content provider: ``<provider>``
Activity, service, content provider를 manifest에 정의하지 않으면 시스템이 컴포넌트의 존재를 알 수 없고, 따라서 실행될 수 없다. 그러나 broadcast receiver는 manifest에 정의하지 않아도 ``Context.registerReceiver()``로 동적으로 정의할 수 있다.
컴포넌트의 범위 설정
Activity, service, broadcast receiver를 실행할 때 ``Intent``를 사용한다. 컴포넌트의 이름을 명시적으로 지정하면 explicit intent이고, 이름을 직접 지정하는 대신 컴포넌트가 수행할 작업의 종류만을 지정하면 implicit intent이다. Implicit intent를 보내면 시스템이 해당 intent가 지정하는 작업을 할 수 있는 앱을 찾고, 그러한 앱이 여러 개라면 사용자가 앱을 선택한다.
주의: Service를 실행할 때에는 explicit intent를 사용하자. 백그라운드로 실행되는 service 특성상 intent를 어떤 service가 처리하는지 확인하기 어렵기 때문에 보안상 문제가 생긴다. Android 5.0(API 21) 이상부터는 ``bindService()``에 implicit intent를 사용하면 exception이 발생한다. 마찬가지로 service에 intent filter를 선언해서는 안 된다(implicit intent를 받을 수 있기 때문).
시스템은 intent를 받으면 각 앱의 intent filter를 확인하며 해당 intent를 실행할 수 있는 앱을 찾는다. Intent filter는 컴포넌트가 처리할 수 있는 intent의 종류를 알려준다.
예를 들어 이메일 작성 activity가 있는 이메일 앱을 개발한다면, 이메일 작성 activity가 "send" intent를 처리할 수 있도록 intent filter를 선언할 수 있다.
<manifest ... >
...
<application ... >
<activity android:name="com.example.project.ComposeEmailActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:type="*/*" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
``<intent-filter>``의 ``<action>``에서 intent의 종류를 지정할 수 있다.
앱의 요구사항 정의
앱이 요구하는 하드웨어/소프트웨어 기능을 manifest에 정의할 수 있다. 컴포넌트 부분과 다르게 이 부분은 시스템이 직접 참고하지는 않지만, Google Play에서 앱을 분류할 때 참고자료로 활용하는 등 외부 서비스에서 참조할 수 있다.
예를 들어, 앱이 카메라 기능을 사용한다는 것을 다음과 같이 나타낼 수 있다.
<manifest ... >
<uses-feature android:name="android.hardware.camera.any"
android:required="true" />
...
</manifest>
Google Play에서는 ``<uses-feature>``에 선언된 기능을 지원하지 않는 기기에 앱을 설치할 수 없다. 그러나 해당 기능이 필수가 아니라면 ``android:required="false"``로 선언할 수 있고, 런타임에서 카메라가 있을 때와 없을 때를 처리하면 된다.
리소스
앱은 코드뿐만 아니라 이미지, 오디오, 애니메이션 등 다양한 리소스로 구성된다. 리소스 파일을 활용하면 화면 크기에 맞는 해상도의 이미지를 제공하고, 언어별 문구를 제공하는 등 앱을 기기별로 최적화할 수 있다.
각 리소스에는 유일한 정수 ID가 할당된다. 그러나 코드에서 ID 값을 직접 활용하기는 어려우므로, 대신 리소스와 ID를 매핑한 ``R`` 클래스를 활용한다. 예를 들어 ``res/drawable``에 저장된 ``logo.png``의 ID는 ``R.drawable.logo``로 접근할 수 있다.
리소스를 활용하면 기기별 설정에 맞는 서비스를 제공할 수 있다. 위에 말했던 것처럼 언어별로 다른 문구를 제공하는 것이 대표적. 기기별로 서로 다른 레이아웃을 사용할 수도 있다.
물론 Compose를 활용하면 레이아웃이나 애니메이션을 XML로 선언할 일은 거의 없어진다.
참고자료
다음 글부터는 activity를 시작으로 좀 더 자세하게 공부해 보자.