アプリを構成する最上位の構成要素がアプリケーションコンポーネント(App component)です。
アプリケーションコンポーネントの概要をまとめます。
目次
アプリケーションコンポーネントとは
アプリケーションコンポーネントはアプリを構成する最上位の構成要素です。
次の4つがあります。
コンポーネント | 概要 |
---|---|
Activity | アプリケーションの画面と処理を構築 |
Service | アプリケーションの画面を持たない処理を構築 |
Broadcast Receiver | システム全体の同時通報の処理を構築 |
Content Provider | アプリケーションのデータを公開(共有) |
アプリは必ず一つ以上のActivityを持ち、必要(アプリに臨む機能)に応じて他のコンポーネントを組み合わせて作ります。同じコンポーネントが複数あっても構いません。
どのようなコンポーネントを持つかで、端末内におけるアプリの役割が決まります。
例えば電話帳アプリは、Activityが電話番号の登録と管理を行い、Content Providerが電話番号のデータベースを他のアプリへ公開します。
通話アプリが電話帳アプリから電話番号を取得できるようになり、相手を指定するだけで電話をかけることができます。番号を入力する必要が無くなるので便利です。
このように端末内で「電話帳アプリ」は、システムや他のアプリから見たい場合の「電話番号の管理と提供」という役割を担うわけです。
コンポーネントの連携と利点
端末内でコンポーネントは連携して動作します。「連携」とはコンポーネントが互いに呼び出し合いながら協調して動くという意味です。
最も多用される連携はActivityの呼び出しでしょう!
画面が狭い携帯端末は、より多くの情報をユーザに提供するために、頻繁に画面を切り替える特徴を持つアプリになります。
この「画面の切り替え」がコンポーネント(Activity)の連携で成り立っています。
連携はアプリ内に限りません。アプリ外と連携が可能です。つまり、他のアプリが持つコンポーネントを呼び出せるということです。
例えば、名刺管理アプリを考えます。
氏名と会社名に加えて顔写真を登録出来たら、再会した時に氏名を間違えるミスが減らせるかもしれません。でも、顔写真を撮るためだけに名刺管理アプリへカメラ機能を実装することは大きな無駄です。
このような時、名刺管理アプリからカメラアプリのコンポーネントを呼び出してしまえば解決できます。
カメラアプリに顔写真の撮影は任せて、撮影した写真のみを名刺管理アプリで受け取り、使用すれば良いのです。
コンポーネントの呼び出しの流れ
コンポーネントの呼び出しは、次にあげるメソッドで行われます。
呼び出しメソッド | コメント | |
---|---|---|
Activity | Context#startActivity( intent ) | コンポーネントから結果を返さない |
Context#startActivityForResult( intent, requestCode ) | コンポーネントから結果を返す(※) | |
Activiry Result API | コンポーネントから結果を返す(※) startActivityForResultのラッパー |
|
Service | Context#startService( intent ) | |
Context#bindService( intent, ... ) | コンポーネントに接続 プロセス間通信(IPC)により関数を実行 |
|
Broadcast Receiver | Context#sendBroadcast( intent ) | 複数コンポーネントを同時に起動 起動は順不同 |
Context#sendOrderBroadcast( intent, ... ) | 複数コンポーネントを順番に起動 起動はプライオリティ順 |
|
Content Provider ( Content Resolver ) | ContentResolver#query( uri, ... } ContentResolver#insert( ... ContentResolver#delete( ... ContentResolver#update( ... | Provider:データの提供側 Resolver:データの取得側 |
※startActivityForResultはライブラリandroidx.activity:activity≧1.20で非推奨 代わりにActiviry Result APIの使用を推奨 |
どの呼び出しも、引数に「起動したいコンポーネント」を指定したIntent(ProviderはUri)を持ちます。
Intentを受け取るのはシステムです。
システムはIntentに含まれる「起動したいコンポーネント」を参照し、コンポーネントのリストから要望に合うものを選びます。これを「インテントの解決」と呼びます。
そして、システムは選んだコンポーネントのインスタンスを作成して、そのインスタンスを起動します。
コンポーネントのリスト
「呼び出しの流れ」で示したようにコンポーネントを起動するのはシステムです。
起動に際して、システムはコンポーネントを知っている必要があり、端末内にあるすべてのコンポーネントのリストを持っています。
そのリストは、OSの起動時またはアプリのインストール時に、アプリのAPKファイルに含まれるマニフェストファイル(AndroidManifest.xml)から収集されたものです。
ですので、アプリの所持する全てのコンポーネントは、マニフェストファイルへ記載されている必要があります。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... > <application ... > <provider android:name=".XXXProvider" android:enabled="true" android:exported="true" /> <receiver android:name=".XXXReceiver" android:enabled="true" android:exported="true" /> <service android:name=".XXXService" android:enabled="true" android:exported="false" /> <activity android:name=".XXXActivity" android:enabled="true" 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> </application> </manifest>
Android Studioを使ってコンポーネントの雛形を自動生成すれば、記述をマニフェストファイルへ追加してくれるので、あまり意識することが無いかも知れません。
コンポーネントの呼び出しの許可・拒否
コンポーネントの呼び出しを許可・拒否する仕組みがあります。
セキュリティ確保のため、またはアプリ自身の都合のため、呼び出されたくないコンポーネントがあるからです。
許可・拒否はマニフェストファイルに記述するEnabledとExported属性で行います。
属性の基本的な動作は表の通りです。
Enabled | インスタンス化 | Exported | アプリ内から 呼出し | アプリ外から 呼出し |
---|---|---|---|---|
true | ○ (記述なしの場合) | true | ○ | ○ |
false | ○ | × | ||
false | × | true | × (インスタンス化できないので全てにおいて拒否) |
|
false | ||||
※○:許可、×:拒否 |
無い場合は下記のようなメッセージを表示してビルドできません。
※Android Studio Chipmunk | 2021.2.1を利用
As of Android 12, android:exported must be set; use true to make the activity available to other apps, and false otherwise. ----- Android 12 以降、android:exported を設定する必要があります。 アクティビティを他のアプリで利用できるようにする場合は true を使用し、それ以外の場合は false を使用します。
Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined. ----- Android 12 以降を対象とするアプリでは、対応するコンポーネントにインテント フィルタが定義されている場合、「android:exported」に明示的な値を指定する必要があります。
android:exportedが無い場合、exportedのデフォルトはintent-filterの有無により変化してしまうため、間違いに気づき難く、思わぬ不具合に見舞われるリスクを負うことになります。
従って、targetSDK < 31であったとしても、android:exportedを必ず明記すべきだと思います。
コンポーネントを呼び出す方法のタイプ
コンポーネントを呼び出す方法は2つのタイプあります。
タイプ | 起動したいコンポーネント | コメント |
---|---|---|
明示的 | コンポーネント名を指定 起動対象は必ず一つに確定 | コンポーネント名が明確である場合 |
暗黙的 | コンポーネントの条件を指定 起動対象は複数の場合あり | コンポーネントの条件は一般的な項目 |
違いは「インテントの解決」におけるコンポーネント選択の動作です。
Intentへ指定する「起動したいコンポーネント」の内容で変わります。
明示的な呼び出し
明示的な呼び出しは、コンポーネント名(パッケージ+クラス名)を指定して、呼び出し対象のコンポーネントを選択します。
このように、明確な対象を指定するIntentを「明示的なインテント」と呼びます。
予め、対象のコンポーネント名を知っている必要があります。
インテントの解決では、対象のコンポーネントは必ず一つに確定します。
コンポーネント名の指定
Intentへ起動したいコンポーネントのコンポーネント名を指定します。
val _intent = Intent(context: Context, cls: Class) val _intent = Intent().setClassName(packageName: String, className: String) val _intent = Intent().setClassName(context: Context, className: String) val _intent = Intent().setClass(context: Context, cls: Class)
暗黙的な呼び出し
暗黙的な呼び出しは、コンポーネントの条件を指定して、呼び出し対象のコンポーネントを選択します。
このように、漠然とした対象を指定するIntentを「暗黙的なインテント」と呼びます。
コンポーネントの条件は具体性がなく、一般的な項目(電話をかける、コンテンツを表示する、など)が使われます。
インテントの解決では、コンポーネントの情報と条件が比較されて、一致したものが選択されます。
条件が緩ければ、対象のコンポーネントは複数になることもあります。
複数になった時は最終的な判断をユーザに求めます。
コンポーネントの定義
コンポーネントの情報・条件を定義するために、次のようなパラメータを使います。
概要 | |
---|---|
action | コンポーネントの動作を示す文字列 |
【独自アクションの例】 "com.example.action.DEMO"(パッケージ+アクション名) ※注意:他のアクションと重複しない文字列であること 【一般的なアクションの例】 "android.intent.action.ACTION_DIAL":電話をかける "android.media.action.ACTION_IMAGE_CAPTURE":写真を撮る |
|
category | コンポーネントの種類を示す文字列 |
【独自カテゴリの例】 "com.example.category.SAMPLE"(パッケージ+カテゴリ名) ※注意:他のカテゴリと重複しない文字列であること 【一般的なカテゴリの例】 "android.intent.category.DEFAULT":標準的(最も普通)に起動 "android.intent.category.LAUNCHER":ランチャーから起動 |
|
data | actionの実行対象であるデータ ・URIで指定(<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]) ・MIMEタイプを追加で指定可能 |
【URIの例】 Uri.parse("tel:03177"):通話アプリで都内の天気を聞く(市外局番+177) Uri.parse("myfile://com.example.sample:55000/sdcard/photo"):アプリ固有のURI |
コンポーネントの情報
マニフェストファイルのintent-filter要素へaction、category、data子要素を定義します。
※action、category、data:複数個の指定が可能
<activity android:name=".XXXActivity" ...> <intent-filter> <action android:name="string" /> <category android:name="string" /> <data android:scheme="string" android:host="string" android:port="string" android:path="string" android:pathPattern="string" android:pathPrefix="string" android:mimeType="string" /> </intent-filter> </activity>
コンポーネントの条件
Intentへ起動したいコンポーネントの条件action、category、dataを指定します。
※category:複数個の指定が可能
val _intent = Intent(action: String) val _intent = Intent(action: String, uri: Uri) val _intent = Intent().apply { setAction(action: String) addCategory(category: String) setData(uri: Uri) // setDataとsetTypeは同時に使えない setType(mime: String) // ⇒ 同時に使う場合はsetDataAndTypeを使用 setDataAndType(uri: Uri, mime: String) }
呼び出しの例1:Activity+明示的
Activityの明示的な呼び出しの例です。
1つはアプリ内(AのMain⇒AのDemo)から、もう一つはアプリ外(BのMain⇒AのDemo)から呼び出しています。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.sample_app"> <application ... > <activity android:name=".DemoActivity" android:enabled="true" 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> </application> </manifest>
class MainActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { ... findViewById<Button>(R.id.btnDemo).setOnClickListener({ val _intent = Intent(this, DemoActivity::class.java) startService(_intent) }) ... } ... }
class MainActivity : AppCompatActivity() { ... private val targetAppPkg = "com.example.sample_app" override fun onCreate(savedInstanceState: Bundle?) { ... findViewById<Button>(R.id.btnDemo).setOnClickListener({ val _intent = Intent().setClassName( targetAppPkg, "${targetAppPkg}.DemoActivity" ) startActivity(_intent) }) ... } ... }
呼び出しの例2:Activity+暗黙的
Activityの暗黙的な呼び出しの例です。
1つはアプリ内(AのMain⇒AのDemo)から、もう一つはアプリ外(AのMain⇒BのDemo)から呼び出しています。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.sample_app"> <application ... > <activity android:name=".DemoActivity" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="com.example.action.DEMO" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <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>
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.test_app"> <application ... > <activity android:name=".DemoActivity" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="com.example.action.DEMO" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <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>
const val ACTION_DEMO = "com.example.action.DEMO" class MainActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { ... findViewById<Button>(R.id.btnDemo).setOnClickListener({ val _intent = Intent(ACTION_DEMO) .addCategory(Intent.CATEGORY_DEFAULT) startService(_intent) }) ... } ... }
以下は、起動するコンポーネントの最終的な判断をユーザに求めています。action=”com.example.action.DEMO”の条件に一致したコンポーネントが複数あるためです。
ユーザによりクリックされたコンポーネントが起動します。
関連記事: