157
151

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

昔ながらのプログラマが躓いた Android アプリ開発のポイント

Posted at

これって何の話?

私は元々 C/C++ や Java で PC のデスクトップまたはコマンドラインで動作するアプリケーションを主に書いていたのですが、いまは Android アプリを書くことが多いです。
昔ながらのデスクトップアプリやコマンドラインアプリの開発から入ったプログラマから見ると、Android アプリの開発には大きなカルチャーギャップがあります。それが原因で Android 開発が思うように行かなかったり挫折しちゃってる人もいるんじゃないかなと思ったりします。
そこで、そんな昔ながらのプログラマのために、押さえておきたい Android 開発のポイントを書いてみたいと思います。

エントリポイントはどこだ?

昔ながらのプログラマにとってコンピュータプログラムは「始まりと終わりが明確にあるもの」です。
たとえば以下のような C のプログラムを見てみましょう。

#include <stdio.h>

int main(int argc, char* argv[]){
    printf("Hello, world!");
    return 0;
}

このプログラムは main 関数の最初から始まり main 関数から抜けることで終わります。これ以上ないほどに明確です。
GUI なプログラムも同様です。たとえば X window system を使う場合は以下のようなコードになるでしょう。

#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Xaw/Label.h>

int main(int argc, char* argv[]){
    XtAppContext context;
    Widget root, label;

    root = XtVaAppInitialize(&context, "Hello, world!", NULL, 0, &argc, argv, NULL, NULL);
    label = XtVaCreateManagedWidget("Hello, world!", labelWidgetClass, root, NULL);
    XtRealizeWidget(root);
    XtAppMainLoop(context);
    return 0;
}

こちらも main 関数の最初から始まりイベントループを抜けて main 関数から抜けることで終わります。

一方 Android ではどうなるでしょうか?
とりあえず Android Studio で新規プロジェクトを作成してみましょう。プロジェクト作成時にはいくつかのテンプレートを選べますが、ここでは Empty Activity を選んでみます。するとなんだか色々ファイルが作成されますが、画面には以下の内容の MainActivity.kt (Kotlin の場合)が表示されます。

MainActivity.kt
package test.app.helloworld

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

ここで以下のように思ってしまう人がいてもおかしくないでしょう。

色々と勝手にファイルが作成されているのが気になるけれど、たぶんこの onCreate() がエントリポイントなんだろう!
ということはこの onCreate() から抜けるとアプリが終了するに違いない!

残念ながら違います。

エントリポイントもイベントループもない(見えない)

Android アプリにも「始まり」はあります。ですがそれは上のコードにある MainActivity#onCreate() ではありません。このメソッドはアプリが持つ画面のうちの一つが作られるときに呼ばれるハンドラであって、アプリが始まるときに呼ばれるものではありません。
ではアプリが始まるときに呼ばれるメソッドはどこなのか?

実は Android アプリの開発では、C 言語で言う main 関数に相当するエントリポイントを書くことはありません。OS の内部処理の話をすると、Android アプリは独立した Linux プロセスとして動作しますので内部的には main 関数に相当するエントリポイントは存在するはずですが、Android OS (Android framework) がそれを隠蔽しています。そのためアプリ開発者はエントリポインタを書くことはありません。同様にイベントループも OS が隠蔽しているので書くことはありません。アプリ開発者はイベントループからディスパッチされるイベントのハンドラを書くことに集中することになります。

うーん、でもそれってイマドキ当たり前なんじゃない?

そうですね。エントリポイントやイベントループを隠蔽するのはイマドキの GUI フレームワークとしてはさして特別なことではありません。ですがそんなフレームワークでも、イベントループにアクセスする手段が提供されていることが多いと思います。
それに対して Android は、アプリがイベントループにアクセスする手段を提供していません。そのため他のプラットフォームでは可能な、独自のイベントループを作るなんてことはできません。たぶん。

アプリ開始のイベントハンドラは Application#onCreate()

Android アプリの開発ではイベントハンドラを書いていくことになる、ということは分かっていただけたと思います。
では改めて、アプリ開始時に呼ばれるハンドラはどこでしょうか?
それは android.app.Application クラスを継承したクラスの onCreate() メソッドです。

え?そんなクラスどこにもないよ?

はい、テンプレートから作成されたプロジェクトにはそのようなクラスのソースはありません。そのような場合、Android は android.app.Application クラスの実装をそのまま使います。その結果、プログラマには「アプリ開始時に呼ばれるハンドラ」がないように見えてしまうわけです。
Android アプリの開発では、ほとんどの場合、このハンドラを意識する必要はありません。ですが場合によっては「アプリ全体としての初期化処理」を書きたい時があります。そのような場合は android.app.Application クラスを継承したクラスを実装して AndroidManifest.xml 内でそのクラスを指定します。

MainApplication.kt
package test.app.helloworld

import android.app.Application
import android.util.Log

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        Log.d("MainApplication", "Start application!")
    }
}
AndroidManifest.xml
    <application
        android:name=".MainApplication"

アプリの終了は OS におまかせ

プログラムの始まりは分かりました。では終わりはどうでしょうか?
X window system などでは画面に表示する Window がなくなるとイベントループから抜けてアプリが終了する仕組みになっていることが多いと思います。
それでは Android もそれと同様に、画面に表示するものがなくなると自動的にアプリが終了するのでしょうか?

この点はちょっと答えにくいところです。
「プログラムの終了=プロセスの終了」と考えるのなら答えは NO です。Android では、すべての画面(Activity)が終了しても、アプリのプロセスは即座には終了しません。
ですがすべての画面(Activity)やサービス(Service)が終了しているなら、そのアプリは事実上何もしなくなります。その意味では、アプリは終了していると見なすこともできると思います。

Android では、アプリプロセスの終了はアプリ側ではコントロールできません(コントロールしてはいけません)。アプリのプロセスは「OS が終了させたいときに終了する」ことになります。アプリ側で明示的に自身を終了させることはできません。OS によって勝手に終了させられてしまいます。

そんな横暴な!

その気持ちはよく分かります。でも Android はそういう仕組みになっているんです。
とはいえ、フォアグラウンドのアプリ(現在動作中のアプリ)がいきなり終了させられることは滅多にありません。通常はバックグラウンドに行って長時間使われていないアプリなどが終了の対象になります。そこらへんは OS がうまくやってくれるわけです。

明示的にアプリのプロセスを終了できないという事実は、昔ながらのプログラマから見ると非常に奇妙に思えてしまいます。しかし以下の点を考えてみるとこの仕組みは合理的にも思えます。

  • スマホでは「アプリを終了させる」操作をすることはほとんどない。
    • PC では「使い終わったアプリは終了させる」のが当たり前だけど、スマホにはそういう文化がない。
    • ホームボタンを押してホーム画面に戻った場合でも、再度アプリを起ち上げると「ホーム画面に戻る直前の画面が復元される」ことが期待されるのがスマホである。
    • アプリの切り替えをした場合も同様である。
    • そもそもユーザーが明示的にアプリを終了させるには「いまどんなアプリが起動しているのか」をユーザーが意識する必要がある。画面が広い PC ならそれができるけど画面が狭いスマホでは困難である。

つまり、スマホでは「ユーザー操作によるアプリの終了」は期待できないわけです。であるなら OS が適切なタイミングでアプリを終了させるのは合理的ですよね。

OS によって勝手に終了させられてしまうということは、アプリ開発においては「いつ終了させられても大丈夫なように実装する必要がある」ことを意味します。基本的には以下のような手法を用います。

  • 状態の保持が必要なら状態変数を永続化する。
    • 各種ライフサイクルメソッド(Activity#onSaveInstanceState()Activity#onStop()など)を実装して内部ストレージに状態を保存する。
    • ↑で保存した状態をライフサイクルメソッド(Activity#onRestoreInstanceState()Activity#onStart()など)を実装して復元する。
    • 特定の Activity や Service に依存せずアプリ全体に渡って使われるデータについては、必要に応じて内部ストレージに都度保存する。

また、アプリのプロセスを明示的に終了させることはできないと書きましたが、Activity を終了させることは普通にできます。Activity とは何かという話はあとでしますが、簡単に言うと「ひとつの画面を表すコンポーネント」です。アプリは複数の画面、つまり Activity を持つことができ、各 Activity は明示的に終了させることができます(Activity#finish())。そのためすべての Activity を終了させることで事実上のアプリ終了状態にすることは可能です。

Activity とか Service ってなんだ?

これも疑問に思う人は結構いるのではないでしょうか。Service については名前から何となくイメージできても Activity については「ふぅ~ん?」という感じになってしまう人が多そうな気がします。

Activity は、上にも書きましたが、ひとつの画面を表すコンポーネントです。画面の状態をすべて保持し、その画面に発生するイベントを受け取り、処理します。アプリ開発者は android.app.Activity の派生クラスを作成し、そのクラスのインスタンスに状態変数を保持させるようにし、イベントハンドラを実装することで目的の画面を実現します。

Service は画面を持たないコンポーネントで、主に画面に依存しないバックグラウンド処理をするために使われます。アプリ開発者は android.app.Service の派生クラスを作成し、そのクラスのインスタンスに状態変数を保持させるようにし、イベントハンドラを実装することで目的のバックグラウンド処理を実現します。

アプリは複数の Activity と複数の Service を持つことができます。Activity だけのアプリを作ることもできますし、Service だけのアプリを作ることもできますし、Activity と Service 両方を持つアプリを作ることも当然できます。

Activity の中から別の Activity を作って「画面遷移」を実現することができます。
Activity の中から Service を作ってまとまった処理をバックグラウンドで実行させることもできます。
Service の中から別の Service を作って異なる処理を並列処理させることもできます。
Service の中から Activity を作って画面を表示させることもできます(Android 10 からは制約あり)。

ふ~ん。Activity が画面に紐づくことは分かったよ。
でも Service って必要かな? スレッドを作ってそのスレッドの中でバックグラウンド処理すれば良いんじゃない?

そう、スレッドを作ることは普通にできますし、それを使ったバックグラウンドでの並列処理も可能です。
でも、アプリが勝手に作ったスレッドは OS (Android framework) の管理対象外です。Activity や Service がすべて終了したアプリは OS によって強制的にそのプロセスを終了させられてしまう可能性が高まります。その場合、仮にスレッドが残っていてもお構いなしに終了してしまいます。

だからスレッドは、通常は Activity 内、または Service 内で完結する処理を実行する形で使います。たとえばある Activity 内で開始したスレッドはその Activity が終了するタイミングまでに終了させるか、それが不可能な場合は可能な限り早く終了するようにします。Activity が終了した以上、そのスレッドもいつ強制的に終了させられるか分からないのですから、それを考慮した実装にする必要があります。1

それに対して Service は OS の管理対象です。Service は、処理が終了するまで強制的に終了させられることは(あまり)ありません。2 だから Activity に依存しない処理や Activity が終了しても続けたい処理は、Service として実装するわけです。

ApplicationActivityServiceを所有しているってこと?

ここまでの話を聞けば、以下のように考えると人もいると思います。

Applicationクラス(またはその派生クラス)のインスタンスが、ActivityServiceのインスタンスを保持、所有するってことだね!

コードで書くとこんな感じでしょうか。

これは動きませんぜ
class MainApplication : Application() {
    private var mainActivity: MainActivity? = null

    override fun onCreate() {
        super.onCreate()

        mainActivity = MainActivity()
        startActivity(mainActivity)
    }
}

Android 以外のプラットフォームの開発をしていた人から見れば「十分あり得そうなコード」ではないでしょうか?
でも Android では、こういうことは許されません。ActivityServiceのインスタンス化は OS の仕事であって、アプリが勝手にやってはいけないんです。

じゃあどうやって Activity や Service を呼び出すの?

Intent を使います。

初めて Android アプリの開発をする人がつまずくポイントの一つに Intent の存在があると思います。他のプラットフォームではあまり見ない概念ですよね。
たとえば Activity1Activity2 の二つの画面があるアプリがあるとして、Activity1 から Activity2 に画面遷移したい場合は以下のようなコードを書くことになります。

class Activity1 : AppCompatActivity() {
    fun callActivity2() {
        val intent = Intent(this, Activity2::class.java)
        intent.putExtra("param1", "some data")
        startActivity(intent)
    }
}

このコード(callActivity2()メソッド)は以下のことをしています。

  1. 呼び出し先のクラス(Activity2)の情報を保持した Intent を作成します。
  2. 呼び出し先に渡したいパラメータを Intent にセットします。
  3. OS に Intent を送ります。

すると OS は以下のことをしてくれます。

  1. 送られてきた Intent の情報から呼び出すべきクラスを選定します。今回は明示的にクラスを指定しているので Activity2 クラスが選ばれます。
  2. そのクラスをインスタンス化します。
  3. インスタンス化された Activity に、呼び出し元から送られてきた Intent を渡します。
  4. インスタンス化された Activity をフォアグラウンドにして表示します。

これにより画面遷移が実現されます。呼び出し先の Activity では Activity#getIntent() メソッドで受け取った Intent を取得できるので、その Intent からパラメータを取り出すことが可能です。

        val param1 = getIntent().getStringExtra("param1")

Service を呼び出す場合も同様です。たとえば Activity1 から Service1 を呼び出すには以下のようなコードが必要になります。

    fun callService1() {
        val intent = Intent(this, Service1::class.java)
        intent.putExtra("param1", "some data")
        startService(intent)
    }

見ての通り Activity のときとほぼ同じですね。3

上記の例では呼び出し先の Activity や Service のクラスを明示的に指定していましたが、明示する代わりにカテゴリー等を指定することもできます。
また他のアプリに実装されている Activity や Service を呼び出すことも可能です。
Intent は、Activity/Service 間での情報のやり取りをするための媒体、と考えると分かりやすいです。

ここで重要なのは、Activity にしろ Service にしろ、直接呼び出すのではなくわざわざ Intent という概念をクッションとして挟んでいるということです。

この仕組みでは呼び出し元は呼び出した Activity や Service のインスタンスを受け取ることはできません。呼び出された側も呼び出し元のインスタンスを受け取れません。そのため互いにインスタンスメソッドやインスタンス変数にアクセスすることができません。Activity/Service 間で連携が必要な場合は、上記の例のように Intent にパラメータをセットして渡すようにしたり、別の仕組み(startActivityForResult()メソッドで結果情報を返すようにする、Applicationの派生クラスを介してやり取りする、Broadcast を利用する等)を使うことになります。

さらに、Intent にセットできるパラメータ(Intent を介してやり取りできるデータ)は Java のプリミティブ型や文字列型、それらの配列型などです。それら以外の型のデータは直列化して渡す必要があります。ということは、オブジェクトのインスタンスをそのまま渡せるわけではないということです。

これのお蔭で各 Activity/Service は高いレベルで疎結合になります。

こんなアーキテクチャになった理由は色々あるようですが、Android はもともとメモリが少ないハードウェアもターゲットにしていたというのが大きな理由だと思います。
メモリが少ない端末では効率よくメモリを解放していく必要があります。各 Activity が疎結合(互いの直接参照がない状態)なら、バックグラウンドに行った Activity はいつでも破棄し、メモリを解放することができます(もちろん例外はあります)。これにより、アプリ自体を落とすことなく、効率よくメモリを解放できます。これがこのアーキテクチャの大きなメリットだと思います。
もっともメモリが有り余っている近年のハードウェアでは、このメリットを享受することはあまりないかも知れません。。。

そもそも「アプリの起動」って?

なるほど了解だ。てことは Application クラスの派生クラスを作って、その onCreate() メソッド内で最初に表示する画面の Activity を指定した Intent を作って startActivity() すればいいんだな!?

これまでの話の流れからこのように考えるのは自然だと思いますが、違います。
先にも書きましたが Application の派生クラスを作ることはあまりありません。そんなことをしなくても最初の画面の Activity を起動することができます。それには AndroidManifest.xml 内で、最初に起動させたい Activity を指定します。

AndroidManifest.xml
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

この記述はプロジェクトをテンプレートから作成したときに勝手に作られたそのままのものです。この記述は MainActivity クラスの設定を定義しているのですが、重要なのは <intent-filter> 要素内の <action android:name="android.intent.action.MAIN" /> という記述です。この記述により、MainActivity が最初に起動するべき Activity であることを指定しています。
(ちなみにその下にある <category android:name="android.intent.category.LAUNCHER" /> という記述は、MainActivity がランチャーから起動できる Activity であることを指定しています)

アプリが起動されるとき、 OS はこの記述が「引っ掛かる」ような Intent を作成して startActivity() を実行します。その結果、起動するべき Activity として MainActivity クラスが特定され、 MainActivity がインスタンス化され表示されます。これは OS がやってくれることなのでアプリ側でわざわざコーディングする必要はないんです。

んん? startActivity() って、あくまで「 Activity を起動する」メソッドだよね?
「アプリを起動する」のはまた別なんじゃないの?

ここはちょっと分かりにくいところです。Android アプリの開発では「アプリの起動」はあまり意識しません。代わりに、アプリ内にある Activity や Service の起動を意識します。

たとえば自アプリから他のアプリを起動することを考えるとしましょう。他のアプリを起動するには以下のようなコードを書くことになります。

        val intent = packageManager.getLaunchIntentForPackage("test.app.other_app")
        startActivity(intent)

PackageManager#getLaunchIntentForPackage() は引数に指定されたアプリの、最初に起動するべき Activity を特定する Intent を返します。だからその Intent を startActivity() に渡すことで、対象のアプリの最初の Activity が立ち上がります。つまり「最初の Activity が立ち上がる」イコール「アプリが立ち上がる」と見なすわけです。

ええっ!? でもそのアプリはまだプロセスが立ち上がってないんじゃないの?
プロセスが立ち上がっていないアプリを対象に Intent を送っても良いってこと??

そうです。Intent を送るときに、送り先のアプリが起動しているか否かを考慮する必要はありません。なぜなら**「もしも送り先のアプリのプロセスが立ち上がっていなければ立ち上げる」という処理を OS がやってくれちゃうからです。** つまり送り先のアプリのプロセスが起動していない場合、そのアプリの Activity に対して startActivity() すると以下のような処理が走ることになります。

  1. アプリのプロセスが起動する。
  2. Application クラス(またはその派生クラス)がインスタンス化され Application#onCreate() が呼ばれる。
  3. Intent で指定された Activity のクラスがインスタンス化され Activity#onCreate() が呼ばれ、フォアグラウンドに表示される。

これが Android における一般的な「アプリの起動」です。

さらに、他のアプリの特定の Activity を直接起動させることも当たり前のようにできてしまいます。パラメータを渡すことも可能です。

        val intent = Intent().apply {
            setClassName("test.app.other_app", "test.app.other_app.Activity2")
            flags = Intent.FLAG_ACTIVITY_NEW_TASK
            putExtra("param1", "hoge")
        }
        startActivity(intent)

これを実行すると、あたかも自分のアプリ内で普通に画面遷移が起こったように、他のアプリの画面に移ります。もしかしたらユーザーはアプリが切り替わったことに気付かないかも知れません。

同様に他のアプリの特定の Service を直接起動させることも可能です。

このように Android では、アプリ間の境界が非常に薄いと言えると思います。自アプリ、他アプリに関係なく、画面遷移させることができてしまいます。自アプリ、他アプリに関係なく、機能を呼び出せてしまうんです。
他のプラットフォームでもアプリ間の連携はそれなりにできると思いますが、ここまでフレキシブルなことができるのは Android くらいではないでしょうか?
こういうところが Android の面白いところであり、またクセの強いところでもあると思います。

Applicationの派生クラスを介してやり取りするってどういうこと?

前の方のセクションで、Activity や Service 間でデータのやり取りをする方法の一つとして「Applicationの派生クラスを介してやり取りする」ことを挙げました。これはどういうことでしょうか?

先の説明ではApplicationActivityServiceを"所有"しているわけではないと書きました。ですがApplicationActivity, Serviceは以下のような関係があります。

  • 一つのアプリには必ず Application クラス(またはその派生クラス)のインスタンスが一つだけ存在する。
  • すべての Activity および Service のインスタンスは、getApplication() メソッドを使ってその Application クラスのインスタンスを取得できる。

そのためアプリ全体で共有したいデータやオブジェクトは Application の派生クラスのインスタンスに持たせてしまえば良いわけです。

MainApplication.kt
class MainApplication : Application() {
    val sharedHoge = Hoge()
}
Activity1.kt
class Activity1 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_1)
        
        (application as MainApplication).sharedHoge.fuga()
    }
}
Service1.kt
class Service1 : Service() {
    override fun onCreate() {
        super.onCreate()

        (application as MainApplication).sharedHoge.fuga()
    }
}

ただし、言うまでもありませんがこの手法はアプリ内の Activity や Service 間でのデータ共有にしか使えません。他のアプリとデータを共有したい場合は、これまで説明してきた通り Intent にパラメータを持たせるかコンテンツプロバイダという仕組みを使うことになります。

そんな面倒なことしなくても static 変数使えばいいんじゃない?

使っちゃダメです。

Java の static メンバーや Kotlin の object はクラスのインスタンスを指定しなくてもアクセスできるため、それを利用してデータの共有をしようとする人もいると思います。たとえば以下のようなコードです。

SharedData.kt
object SharedData {
    var hoge: String = ""
}
Activity1.kt
class Activity1 : AppCompatActivity() {
    fun onClickFugaButton() {
        SharedData.hoge = "fuga"
    }
}
Activity2.kt
class Activity2 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_2)
        findViewById<TextView>(R.id.textViewHoge).text = SharedData.hoge
    }
}

この例では SharedData.hoge をグローバル変数のように使ってデータを共有しようとしています。他のプラットフォームなら特に問題ないやり方ですが、Android ではこれは許されません。

えっ? でもコンパイルエラーは出ないし普通に動くよ?

はい、エラーは出ません。期待通りに動くこともあります。
でも期待通りに動かないときもあるんです。正常に動いたり動かなかったりするものだから余計に混乱を招きやすいです。

Android のガベージコレクタは一般的な Java のガベージコレクタとは異なり、インスタンスだけでなくクラス自体の情報もメモリからアンロードします。そしてクラスがアンロードされる際に static 変数が保持していたデータも破棄されます。
上のコードでは SharedData.hoge に文字列データを保持させていますが、ガベージコレクタによって SharedData オブジェクト(クラス)がアンロードされる可能性があります。すると保持させていた文字列データも破棄されてしまいます。その後 SharedData.hoge を読み出そうとすると、そのときに再度 SharedData オブジェクト(クラス)がロードされ初期化されるため空文字が返却されます。

他のプラットフォームから Android 開発に移った人が躓く最大のポイントかも知れませんね。
以上の理由から、Android アプリの開発で Java の static 変数や Kotlin の object の用途は以下に限るべきでしょう。

  • 定数定義
  • そのクラスのインスタンスが最低1個生存していることが保証されるコンテキストでの利用

このように Activity や Service 間のデータ共有(データの受け渡し)に static 変数や object を使うことは基本的に避けるべきです。これまで説明してきたように startActivity()startService() に渡す Intent にパラメータとして持たせるか、Application の派生クラスに共有データを持たせるか、SharedPreferences にデータを保管して共有するか、普通にファイルとして保管するなどの方法が使われます。

じゃあたとえば Service から Activity 内のイベントを発火させたいときはどうするの?

これまで Activity や Service を起動する方法を説明してきました。ざっくりおさらいすると、Intent を作って startActivity()startService() メソッドに渡せば良いことは分かってもらえたと思います。

ですが Activity や Service 間の連携させるに当たって「対象の Activity/Service を起動する」だけでは目的を達成できないことがあります。
その例として、Activity から Service を起動し、その Service がバックグラウンド処理を終えて、「処理が終わった」ということを Activity に知らせることを考えてみましょう。
Activity が Service を起動するのはこれまで見てきたように startService() (または startForegroundService())でできます。では Service が Activity に処理の完了を知らせるにはどうすれば良いでしょうか?

この場合、Activity はすでに起動していますので startActivity() を使うのは必ずしも適当ではありません。
このような場合に使われるのが sendBroadcast() メソッドです。このメソッドも startActivity()startService() と同様 Intent を送るためのものですが、送り先は Activity や Service ではなく BroadcastReceiver になります。BroadcastReceiver は Activity や Service の中で生成、登録できるので、それを使って Intent を受け取ります。

MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val myBroadcastReceiver = object: BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            findViewById<TextView>(R.id.textView1).text = "処理終わったっす"
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // BroadcastReceiver を登録する
        LocalBroadcastManager.getInstance(this).registerReceiver(
            myBroadcastReceiver,
            IntentFilter("test.app.actions.SERVICE_FINISHED")
        )
        
        // サービス起動
        startService(Intent(this, Service3::class.java))
    }

    override fun onDestroy() {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(myBroadcastReceiver)
        super.onDestroy()
    }
}
Service3.kt
class Service3: IntentService("Service3") {
    override fun onHandleIntent(intent: Intent?) {
        // いろいろ処理する
        Thread.sleep(1000)

        // ブロードキャストを送る
        LocalBroadcastManager.getInstance(this).sendBroadcast(
            Intent("test.app.actions.SERVICE_FINISHED")
        )
    }
}

ここではブロードキャストの送信先がアプリ内の Activity なので LocalBroadcastManager#sendBroadcast() を使いましたが、他のアプリに送る場合は Context#sendBroadcast() を使います(ContextActivityService の共通の親クラスです)。4 5

まとめ

長くなってしまったのでだいぶ端折りましたが、途中分からないところがあったら適宜ググってもらうということで…。

Android のアーキテクチャは結構独特ですよね。私も最初は、ライフサイクルイベントあたりは「まぁ必要だよね」と理解できましたが、 Activity/Service を連携させようとした途端に「???」になってしまいました。同じところで躓いている人の手助けになると嬉しいです。

  1. 実際には Activity が終了してもアプリ内の他の Activity や Service が残っていればスレッドは生き続けます。ですが Activity や Service は他の Activity や Service にはなるべく依存しないように(疎結合になるように)実装するべきですので、他の Activity/Service の生存を期待した実装にするべきではないでしょう。

  2. Service(Foreground service) は、システム全体がメモリ不足に陥ったときに、OS によって強制的に終了させられてしまうことがあります。これはシステムの不具合などではなく、もともと Android はそういう設計になっているんです。そのため Service を実装する際は、OS によって強制終了させられることがある前提で実装する必要があります。

  3. この例では startService() メソッドを使っていますが、この方法で起動した Service は Android 8.0 以降だと長時間の処理ができません。起動した Service を長時間生き続けさせたい場合は startForegroundService() メソッドで Foreground service として起動する必要があります。

  4. 実のところを言うと、Service からアプリ内の Activity にイベントを送りたい場合は必ずしも BroadcastReceiver を使う必要はなかったりします。というのも、Application の派生クラスを介すことで、Service から Activity のインスタンスメソッドを呼ぶことも可能だからです。ただし、そのようなことをする場合でも、Activity と Service ができる限り疎結合になるよう注意して設計するべきでしょう。

  5. BroadcastReceiver は、OS が発火させるシステムイベントも受け取ることができます。たとえば端末起動直後に何かしらの処理を実行させることが可能です。ただし、Activity も Service も存在しない状態では BroadcastReceiver#onReceive() から処理が抜けた時点でプロセスが終了させられても仕方がないと考えるべきですので、長時間の処理をさせたい場合は BroadcastReceiver#onReceive() 内で Service を起動し、その Service 内で時間のかかる処理を実行するべきでしょう。

157
151
10

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
157
151

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?