アンドロイド スマホのアプリについて

 アンドロイド スマホのアプリで、画面を表示したり、画面の操作を受けたりする部分をアクティビティ(Activity)と呼び、Activityクラスか、Activityクラスのサブクラスの、サブクラスとしてプログラムを書きます。


 アクティビティのonCreate()は、アクティビティがメモリ上に作成される時にのみ呼ばれ、必ず処理内容を書く必要が有り、画面の表示の設定のXMLファイルを指定したりします。


 アクティビティのonCreate()の例

――――――――――――――――――――

class MainActivity : AppCompatActivity() {


  override fun onCreate(savedInstanceState: Bundle?) {

    try {

      super.onCreate(savedInstanceState)

      setContentView(R.layout.activity_main)


      //ここに処理を書きます。


    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    }

  }

}

――――――――――――――――――――


 アクティビティのonStart()は、onCreate()の後で画面を表示する前と、onStop()の画面非表示の後で画面が再表示される前に呼ばれます。


 アクティビティのonResume()は、画面が表示されて操作可能に成った時、画面が再表示されて操作可能に成った時、(別のアプリの画面の背後にいて操作不能であったが)画面が操作可能に成った時に呼ばれます。


 アクティビティのonPause()は、別のアプリの画面の背後で操作不能だが部分的に表示されている時に呼ばれます。


 アクティビティのonStop()は、画面が非表示に成った時に呼ばれます。


 アクティビティのonDestroy()は、ユーザーがアプリを終了した時、Androidが自動でアプリを終了した時、スマホなどの向きが変更された時、マルチ ウィンドウに成った時に呼ばれます。


 アクティビティのonDestroy()の例

――――――――――――――――――――

class MainActivity : AppCompatActivity() {


  override fun onCreate(savedInstanceState: Bundle?) {

    try {

      super.onCreate(savedInstanceState)

      setContentView(R.layout.activity_main)


      //ここに処理を書きます。


    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    }

  }



  override fun onDestroy() {

    try {


      //ここにリソースを解放する処理を書きます。


    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    } finally {


      super.onDestroy()

    }

  }

}

――――――――――――――――――――


 ※アプリの画面をスワイプして消して、ホーム画面のアプリのアイコンを押して、アプリの画面を呼ぶと、onStart()やonResume()が呼ばれずに画面が復元されるようです。


 アクティビティ(Activity)はコンテキスト(Context)を継承しています。

 画面と共にアクティビティが終了されてメモリーから破棄されると、アクティビティのコンテキストも破棄されます。

 アクティビティのコンテキストとは別にアプリケーション コンテキスト(ApplicationContext)が存在します。アクティビティは「getApplicationContext()」や「applicationContext」でアプリケーション コンテキストを取得する事もできます。

 画面と共にアクティビティが終了されてメモリーから破棄されても、アプリケーション コンテキストは破棄されません。そのため、アクティビティがアプリケーション コンテキストを取得して何かをした場合、それらを適切に終了処理して破棄していないと、いつまでもメモリーに残り続けてメモリーを食ってしまう「メモリー リーク」と呼ばれる問題が発生してしまいます。ですから、注意が必要です。



 アンドロイド スマホのアプリで、バックグラウンドで処理する部分を「サービス(Service)」と呼び、Serviceクラスか、Serviceクラスのサブクラスの、サブクラスとしてプログラムを書きます。


 ※「フォア グラウンド サービス」ではない「サービス」は、バック グラウンドのまま30分くらい経つと、アンドロイド システムに強制終了されてしまいます。


 ・startForegroundService(intent: Intent)で、「フォア グラウンド サービス」を起動できます。

 ・「フォア グラウンド サービス」がまだ無い場合は新規作成されます。


 ※「フォア グラウンド サービス」はstartForegroundService(intent: Intent)から5秒以内にstartForeground(flag: Int, notification: Notification)を呼ぶ必要が有ります。


 ※startForeground(flag: Int, notification: Notification)を呼ばないと、startForegroundService(intent: Intent)から5秒が経ったら、アンドロイド システムに強制終了されてしまいます。


 ※startForeground(flag: Int, notification: Notification)に必要である、通知のNotificationクラスは、通知チャンネルのNotificationChannelを作成する必要が有ります。


 ※bindService()で「サービス」へ接続できます。



 アンドロイド スマホのアプリで、アンドロイド システムが自動でスキャンして情報収集している音楽ファイル、画像ファイル、動画ファイル、ダウンロード ファイルの一覧を取得できる部分などを「コンテンツ プロバイダー」と呼びます。



 アンドロイド スマホのアプリで、アンドロイド システムからの通知を受け取ったり、サービスからの通知を受け取ったりする部分を「ブロードキャスト レシーバー」と呼びます。



 「サービス」からアクティビティへ情報を渡す場合は、次のようにします。

 ①アクティビティで、ブロードキャスト レシーバーのサブクラスを作成しつつ、アクティビティ自身(の参照)をブロードキャスト レシーバーのサブクラスに渡し、ブロードキャスト レシーバーのサブクラスをアンドロイド システムに登録します。

 ②「サービス」で、アクティビティへ渡す情報をブロードキャスト通知をします。

 ③アンドロイド システムがブロードキャスト レシーバーのサブクラスを呼びます。

 ④ブロードキャスト レシーバーのサブクラスで、「サービス」からアクティビティへ渡したい情報を受け取り、アクティビティ自身(の参照)に更に渡したり、アクティビティの処理を呼んだりします。



 例えば、アンドロイド システムから音楽の一覧を検索し、画面に音楽の一覧などを表示し、バック グラウンドでも音楽を再生し続け、アンドロイド システムからの通知で有線イヤホンが抜けた時に音楽の再生を一時停止するミュージック プレイヤー アプリはコンテンツ プロバイダー、アクティビティ、「(フォア グラウンド )サービス」、ブロードキャスト レシーバーの4つが必要です。



 例えば、2画面間で一方の画面から他方の画面へ遷移する場合は、画面の数と同じアクティビティ、2つのアクティビティが必要に成ります。


 ある画面とアクティビティから、別の画面とアクティビティへ遷移する場合は、まず、AndroidManifest.xmlに画面遷移の設定を書く必要が有ります。


/home/◯◯◯/AndroidStudioProjects/◇◇◇/app/src/main/AndroidManifest.xml

――――――――――――――――――――

    <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>


    <activity

      android:name=".SubActivity"

      android:parentActivityName=".MainActivity"

    >

      <meta-data

        android:name="android.support.PARENT_ACTIVITY"

        android:value=".MainActivity"

      />

    </activity>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 ◇◇◇はAndroid Studioのプロジェクトの名前です。



 ある画面とアクティビティから、別の画面とアクティビティへ遷移する場合に、別の画面から元の、ある画面へ情報を渡して戻る必要が無い場合は、startActivity(intent: Intent)で別の画面へ遷移し、finish()で元の画面へ戻ります。


――――――――――――――――――――

val intent = Intent(this, SubActivity::class.java)

intent.putExtra(XXX_KEY, "xxx")

startActivity(intent) //別の画面へ遷移

――――――――――――――――――――


――――――――――――――――――――

finish() //元の画面へ戻る

――――――――――――――――――――



 ある画面とアクティビティから、別の画面とアクティビティへ遷移する場合に、別の画面から元の、ある画面へ情報を渡して戻る場合は、registerForActivityResult()で登録したActivityResultLauncherのActivityResultLauncher.launch(intent: Intent)で別の画面へ遷移し、setResult(RESULT_OK, intent)してからfinish()で元の画面へ戻ります。


――――――――――――――――――――

companion object {

  const val XXX_KEY = "user.project.XXX"

}


var xxxEditText: EditText? = null


private var xxxActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->

  if (activityResult.resultCode == RESULT_OK) {

    val intent = activityResult.data


    xxxEditText?.setText(intent?.getStringExtra(XXX_KEY))

  }

}


override fun onCreate(savedInstanceState: Bundle?) {

  try {

    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_main)


    xxxEditText = findViewById(R.id.xxx)


    val intent = Intent(this, SubActivity::class.java)

    intent.putExtra(XXX_KEY, xxxEditText?.text?.toString())

    xxxActivityResultLauncher?.launch(intent) //別の画面へ遷移


  } catch (exception: Exception) {

    Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

    throw exception

  }

}


override fun onDestroy() {

  try {

    xxxEditText = null


    xxxActivityResultLauncher?.unregister()

    xxxActivityResultLauncher = null


  } catch (exception: Exception) {

    Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

    throw exception

  } finally {


    super.onDestroy()

  }

}

――――――――――――――――――――


――――――――――――――――――――

val intent = Intent(this, MainActivity::class.java)

intent.putExtra(XXX_KEY, "xxx")

setResult(RESULT_OK, intent)

finish() //元の画面へ戻る

――――――――――――――――――――



 ちなみに、アプリ外のファイルを読み書きする時に利用するSAF(ストレージ アクセス フレームワーク)は、別の画面から元の画面へ情報を渡して戻る場合と似たような処理を利用します。


――――――――――――――――――――

private var readActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->

  if (activityResult.resultCode == RESULT_OK) {

    val intent = activityResult.data


    val uri: Uri? = intent?.data


    val documentFile = DocumentFile.fromSingleUri(applicationContext, uri!!)


    val fileName = documentFile?.name


    var text = ""


    InputStreamReader(contentResolver.openInputStream(uri!!), "UTF-8").use {

      text = it.readText()

    }

  }


  if (activityResult.resultCode == RESULT_CANCELED) {

  }

}


private var saveActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(StartActivityForResult()) { activityResult: ActivityResult ->

  if (activityResult.resultCode == RESULT_OK) {

    val intent = activityResult.data


    val uri: Uri? = intent?.data


    val documentFile = DocumentFile.fromSingleUri(applicationContext, uri!!)


    val fileName = documentFile?.name


    //contentResolver.openOutputStream()で"wt"モードを指定しないと、書き込む予定の内容よりも、書き込む前のファイルの内容の量のほうが大きい場合、書き込むと、書き込む前のファイルの内容の先頭の一部を置換するような形に成ってしまい、書き込む前のファイルの内容の末尾が切り捨てられずに残ってしまいます。

    OutputStreamWriter(contentResolver.openOutputStream(uri!!, "wt"), "UTF-8").use {

      BufferedWriter(it).use {

        it.write("テキスト ファイルに書き込む内容")

      }

    }

  }


  if (activityResult.resultCode == RESULT_CANCELED) {

  }

}


override fun onCreate(savedInstanceState: Bundle?) {

  try {

    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_main)


    findViewById<Button>(R.id.readButton).setOnClickListener{ view ->

      try {


        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)

        intent.addCategory(Intent.CATEGORY_OPENABLE)

        intent.type = "*/*"

        readActivityResultLauncher?.launch(intent)


      } catch (exception: Exception) {

        Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

        throw exception

      }

    }


    findViewById<Button>(R.id.saveButton).setOnClickListener{ view ->

      try {


        val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)

        intent.addCategory(Intent.CATEGORY_OPENABLE)

        intent.type = "*/*"

        saveActivityResultLauncher?.launch(intent)


      } catch (exception: Exception) {

        Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

        throw exception

      }

    }


  } catch (exception: Exception) {

    Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

    throw exception

  }

}


override fun onDestroy() {

  try {


    readActivityResultLauncher?.unregister()

    readActivityResultLauncher = null


    saveActivityResultLauncher?.unregister()

    saveActivityResultLauncher = null


  } catch (exception: Exception) {

    Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

    throw exception

  } finally {


    super.onDestroy()

  }

}

――――――――――――――――――――


 ちなみに、SAF(ストレージ アクセス フレームワーク)はユーザーがSAFの画面でフォルダと、ファイルの名前(と、それに伴い上書きするかどうか)を決めるので権限の確認は不要です。



 また、SAF(ストレージ アクセス フレームワーク)でユーザーに複数のファイルを選択させる場合は、Intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)を追加し、1つのファイルだけが選択された時はIntent.dataで選択ファイルのURIが取得でき、2つ以上のファイルが選択された時はIntent.clipData.itemCountで選択ファイル数が取得できIntent.clipData.getItemAt(ゼロから始まるインデックス番号).uriで選択ファイルのURIが取得できます。


――――――――――――――――――――

  private var readActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult: ActivityResult ->

    if (activityResult.resultCode == RESULT_OK) {

      val intent = activityResult.data


      var uriList = mutableListOf<Uri>()


      //2つ以上のファイルが選択された場合

      if (intent?.clipData?.itemCount != null) {


        for (index in 0..(intent?.clipData?.itemCount!! - 1)) {

          val uri = intent?.clipData?.getItemAt(index)?.uri as Uri


          uriList.add(uri!!)

        }


      //1つのファイルだけが選択された場合

      } else {

        if (intent?.data != null) {


          val uri = intent?.data as Uri


          uriList.add(uri!!)

        }

      }


      //何らかの処理

    }


    //ファイルが選択されなかった場合

    //

    //ゼロ件のファイルが選択された場合

    //

    if (activityResult.resultCode == RESULT_CANCELED) {

      var uriList = mutableListOf<Uri>()


      //何らかの処理

    }

  }

――――――――――――――――――――


――――――――――――――――――――

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)

intent.addCategory(Intent.CATEGORY_OPENABLE)

intent.type = "*/*"

intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)

readActivityResultLauncher?.launch(intent)

――――――――――――――――――――

  • Xで共有
  • Facebookで共有
  • はてなブックマークでブックマーク

作者を応援しよう!

ハートをクリックで、簡単に応援の気持ちを伝えられます。(ログインが必要です)

応援したユーザー

応援すると応援コメントも書けます

新規登録で充実の読書を

マイページ
読書の状況から作品を自動で分類して簡単に管理できる
小説の未読話数がひと目でわかり前回の続きから読める
フォローしたユーザーの活動を追える
通知
小説の更新や作者の新作の情報を受け取れる
閲覧履歴
以前読んだ小説が一覧で見つけやすい
新規ユーザー登録無料

アカウントをお持ちの方はログイン

カクヨムで可能な読書体験をくわしく知る