Senの競技プログラミング備忘録

こけた問題を自分用の解説で載せる。けんちょんさんのブログを目指したい。質的にも量的にも。こけた問題だけに限定するけど

Android開発 for Kotlin 自分向けのメモ帳 動的な部分

自分向けのメモ。たぶん新入生教育にも開発仲間にも役に立ちそう。(あくまで自分向けのsummaryだが)
静的な部分
sen-comp.hatenablog.com

TODO: Layoutinflateらへんを書く。BottomNavigationViewらへんを書く。

Event Listener, handler

静的にアプリを構築するXMLに、「何々がされたら」、「〇〇をする」という動的要素をつけるためのもの。
Eventが「対象の何々」、Event Listenerが「何々がされたら」に相当し、handlerが「〇〇をする」に当たる。

View.OnClickListenerの継承

さまざまなViewにおいてクリックされた時のEvent Listener。
内部のfun onClick(View? view) : Unitは抽象関数であり、ちゃんと定義しないといけない。このonClick()が押された時の挙動を記述する。つまり、handlerにあたる。

基本的には、Event Listenerのクラスを用意して、その中にhandlerを記述して、最後にonCreate()に取得したViewにsetOnClickListener()でListenerをセットする。

ボタン限定で、Listenerをわざわざ設定しなくても簡易的にListenerみたいなことがやれるonClick()がある。(設定してもいい)

Listenerは1つのActivityにOnClickListenerなら1つだけでいい。(複数のボタンを全部同じListenerにする。)onCreate()内のfindViewById()でIDごとにどのボタンが押されたのかを判定して処理すればいい。

OnClick()式。Button特有のXMLのプロパティで設定する。ボタンの数だけ関数が必要になる。これは静的な機能の解説記事にある。

普通のEvent Listener式をここで紹介する。無名でも有名でもいいのでクラスを定義する。

//クラスの定義 View.OnclickListenerをOverride。OnClick()はvirtualなので定義必須
private inner class helloListener : View.OnClickListener{
    override fun onClick(v: View?) {
        //この中に押された時の処理を書く。
    }
}

onCreate()の中で

//onCreate()の中 もちろん即席クラス作ってもいい。setOnClickListenr(object: View.OnClickListener{...})で
val bt = findViewById<Button>(R.id.show_price)
val bt_listener = helloListener();
bt.setOnClickListener(bt_listener);

AdapterView.OnItemClickListenerの継承

ListViewやSpinnerの1つ1つがクリックされたのを検知するリスナー。ListViewやSpinnerはこっちを使う。
OnClickListenerと似てるように、onItemClick()はabstractなのでoverrideして必ず定義しなければならない。

private inner class ListItemClickListener : AdapterView.OnItemClickListener{
    override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        val item1 = parent.getItemAtPosition(position) as String;
        val item2 = view as String;
        //item1とitem2は同じものを指している。
        //to do something
    }
}

引数の説明をすると

  • parent: タップされたList, Spinner全体のView。AdapterViewはListViewやSpinnerの親クラス。
  • view: タップされた場所のView
  • position: タップされた場所が上から数えて何番目か。0-idx
  • id: Adapterを使用してListViewを設定した時の、データベースの主キーの値。

そして、onCreate()には次のように、

val lvMenu = findViewById<ListView>(R.id.lv1);
lvMenu.onItemClickListener = ListItemClickListener();
//上のように書くことが推奨。まあ下で書いてもいいし、下で書いてもAndroid Studioで上に直していいですか?って聞いてくる。
lvMenu.setOnItemClickListener(ListItemClickListener());

OnClickListenerのsetOnClickListener()は一般メソッドなので、普通に一般メソッド呼び出しでセットしなければならない。
OnItemClickListenerのsetOnItemClickListener()はonItemClickListenerのセッターなので、Kotlinの仕様で=演算子で直接代入できるようになっている。
むしろsetOnItemClickListener()で書いてると、「Use property access syntax」と言われて=で書いてくれと言われる。

Adapter

動的に用意されたデータベースのデータを、ListViewやSpinnerに代入する役割を持つ。
順序としては、

1. ListやSpinnerに埋め込むデータベースを用意する。(基本的にmutableList, mutableMapで)
2. このデータベースをもとにAdapterオブジェクトというものを用意する。
3. ListViewやSpinnerに2. で用意したAdapterオブジェクトをセットする。

各Listのアイテムはシンプルな一行の文字列から、複雑な開発者のオリジナルデザインxmlまで何でも適用可能。
そのためには、レイアウトXMLファイルごとにも設定されているR値を引数に渡す必要があるが、いくつかのよく使うものはAndroid SDK側がすでに用意してくれている。

この時、Android SDK側のLayoutは、android.Rクラスの下にあり、これはプログラマが定義したViewのIDが入ってるRクラスとは別物

import android.R

なんか書いたらSDK側のRクラスの中身しか使えなくなるので、厳禁!

ListViewの1itemに一列の文字列

1. のデータベースでは、基本的にはmutableMapを使って、レイアウトXMLのどのViewにどのデータを割り当てるのかを与えるが、そもそも一列しか表示しないのならmutableListでいい。

下のコードにある

//ステップ1
var dbList = arrayOf("一刀両断", "十二神将", "三跪九叩", "四面楚歌", "五里霧中", "六道輪廻", "七擒七縦", "八 王 子")
//ステップ2
val adapter = ArrayAdapter(applicationContext, android.R.layout.simple_expandable_list_item_1, dbList)
//ステップ3
val lv = findViewById<ListView>(R.id.lv1)
lv.adapter = adapter

ここのArrayAdapter()の第三引数で渡すデータベースだが、overloadされてる関数を見る限りmubtaleListでもArrayでも問題ない。

Simple Adapter(ListViewの1itemに二行の文字列)

先ほどはArray Adapterを使っていたが、これは1itemに一列の文字だけを入れるときに使うものであり、本来はSimple Adapterを使う必要がある。

まず、ListのitemのレイアウトXMLファイルと、突っ込むListViewを用意する必要がある。

  • ListViewの1itemごとに、データのラベル(String)と中身(*)を保持するMutableMap型のMutableMapを用意して、それにデータを入れる。
  • その用意したitem数個だけのMutableMapをMutableListに突っ込んで、データベースとする。
  • MutableMapにおいてのキーと、layoutの割り当てるViewのIDを紐づけるArrayとIntArrayを用意する。
  • SimpleAdapter()にこれを突っ込んで、ListViewに生成したAdapterを入れる。

詳細、特にMutableMapでのキーとLayoutの割り当てるViewのIDの紐づけはソースコードを見るとわかりやすい。

なお、今回使用するレイアウトはこれである。

f:id:Sen_comp:20200625184531j:plain

val database : MutableList<MutableMap<String, *>> = mutableListOf()
val tagOrder : Array<String> = arrayOf("top", "middle", "bottom")
val viewOrder : IntArray = intArrayOf(R.id.itemtextView1, R.id.itemtextView2, R.id.itemtextView3)

database.add(mutableMapOf("top" to "大文字", "middle" to "中文字", "bottom" to "小文字"))
database.add(mutableMapOf("top" to "2行目の一番上", "middle" to "2行目の真ん中", "bottom" to "2行目の下"))
database.add(mutableMapOf("top" to "一刀両断", "middle" to "二束三文", "bottom" to "三寒四温"))

var targetListView = findViewById<ListView>(R.id.mainListView)
targetListView.adapter = SimpleAdapter(
	applicationContext,
    database,
    R.layout.listview_item_layout,
    tagOrder,
    viewOrder
)

完成品イメージ
f:id:Sen_comp:20200625184447p:plain

ViewBinder

データベースのデータを直接Adaptするのではなく、選択的に一部のデータにのみ操作をしたい場合、SimpleAdapter.ViewBinderというものを使う。(参考文献を大いに参考した)

SimpleAdapter.ViewBinderは抽象クラスで、その中にあるabstract boolean setViewValue()をユーザーは実体化させなければならない。これは3つの引数がある。

  • View view -> 内部的には、各itemのごとの構成要件のViewがここの引数に渡される。このViewに対して、どのViewならそのViewに入っているデータの操作を行うor行わないを記述する。
  • Object data -> Viewに入っているdata。型はいろいろあるのでObject
  • String textRepresentation -> dataの文字列表現。絶対にnullではないという保証がある(空文字列はありうる)(by Reference) 基本的にdataはtoString()で変換できるけどできないもの用の変数(つまり普段使うことはなさげ)

そして、Viewのデータに変更があるのならば、trueを返し、そうでなければfalseを返す必要がある。

そうやって継承したクラスを実体化して、AdapterのsetViewBinder()に渡してやれば晴れて完成となる。

//onCreate()内 基本的にデータは上のSimple Adapterのと同じ
database.add(mutableMapOf("top" to "男", "middle" to "中文字", "bottom" to "小文字"))
database.add(mutableMapOf("top" to "女", "middle" to "2行目の真ん中", "bottom" to "2行目の下"))
database.add(mutableMapOf("top" to "男", "middle" to "二束三文", "bottom" to "三寒四温"))

var targetListView = findViewById<ListView>(R.id.mainListView)
var targetListViewAdapter = SimpleAdapter(
    applicationContext,
    database,
    R.layout.listview_item_layout,
    tagOrder,
    viewOrder
)
targetListViewAdapter.setViewBinder(SexualViewBinder())

//ここからがViewBinderを継承したクラスの宣言

inner private class SexualViewBinder : SimpleAdapter.ViewBinder{
    override fun setViewValue(view: View?, data: Any?, textRepresentation: String?): Boolean {
        //itemtextView1にAdaptしたデータはすべて"男"か"女"である。
        if((view!!).id == R.id.itemtextView1){
            var targetTV : TextView = view as TextView
            if(data.toString() == "男"){
                targetTV.setText("male")
            }else {
                targetTV.setText("female")
            }
            return true
        }
        else {
            return false
        }
    }
}

ダイヤログ

f:id:Sen_comp:20200618031420p:plain
(出典:スマホに警告「ウイルスに感染しているので、早急の対応が必要です」が表示される問題と対策について)

こういうOKボタンがついている、メッセージを表示するViewのこと。

ダイヤログの生成に必要なもの。

  • 独立したktファイル(ダイヤログの挙動を動的に記述するためのダイヤログ生成クラス)
  • 飛ぶ前のktファイルに紐づけるもの

ダイヤログ生成クラス

ダイヤログの構成はこのようになっている。
f:id:Sen_comp:20200619030927j:plain

ちなみに、最低限のダイヤログは「コンテンツエリア」と「アクションボタン1つ」のみでよい。

これらのダイヤログ特有の設定を行うには、新しくktファイルを作り、その中のクラスから設定する必要がある。

まず、ダイヤログを表すクラスはDialogFragment(2種類あるが、より使いやすいサポートライブラリのandroid.support.v4.app.DialogFragmentがよりいい)を継承したクラスでなければならない。そして、その中のabstractなonCreateDialog()を実装する必要がある。

onCreateDialog()

onCreateDialog()内でやるべきの手順は以下のようになっている。

1. AlertDialog.Builderオブジェクトを生成する。Dialogを表すのはAlertDialogだが、Builderを作っていろいろ設定していくことになる。
2. 表示の設定をする。先ほどの図のタイトルやコンテンツエリア、などである。詳細はソースコード参照。
3. アクションボタンを設定する。Android Studioにある「ポジティブ」、「ネガティブ」、「中立」の三種類が用意されていて、好きに使うことができる。1つしかないようなものはPositiveである。
4. 設定を終えたAlertDialog.Builderオブジェクトからcreate()メソッドでAlertDialogを作る。(返り値として帰ってくる)
5. 定義を終えたAlertDialogを呼び出す元で呼び出しコードを記述する

詳しい説明はソースコード参照

class dialogtest : DialogFragment(){
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        //DialogBuilderを生成
        val builder = AlertDialog.Builder(activity)

        //Dialogのタイトルの設定 直接打ち込むもよし、string.xmlから参照してもよい。
        builder.setTitle("タイトル")
        builder.setTitle(R.string.titleStr)
        //ダイヤログのメッセージを設定 上と同様に直接打ち込んでもいい
        builder.setMessage(R.string.msg)
        //各種Buttonの設定 文字列は上と同じように扱える。第2引数はボタンを押された時のListener。次セクションで解説
        builder.setPositiveButton("賛成", dialogButtonClickListener())
        builder.setNegativeButton("反対", dialogButtonClickListener())
        builder.setNeutralButton("どっちでもよい", dialogButtonClickListener())
        
        //DialogObjectを生成して、返す
        return builder.create()
    }
}
クリックされた時のListener

先ほどのソースコードのButtonたちのsetterの2つ目の引数にdialogButtonClickListenerクラスのインスタンスを生成して渡したが、このクラスは例によってListenerとして、あらかじめ別記しなければならない。

AlertDialogのButtonのListenerクラスは、DialogInterface.OnClickListenerクラスを継承して作られる。この時、例によってonClick()はabstractなので、具体化して、その中にボタンが押された時の挙動を定義していく。

一般的にAlertDialogには複数のButtonが含まれるとき、普通のButtonのListenerでも話したように、when()文(C, C++, javaでいうとswitch文)で条件分岐させて扱うのが基本である。ソースコード参照。

inner class dialogButtonClickListener : DialogInterface.OnClickListener{
    override fun onClick(dialog: DialogInterface?, which: Int) {
        when(which){
            DialogInterface.BUTTON_POSITIVE->
                //ポジティブボタンなら
            DialogInterface.BUTTON_NEUTRAL->
                //中立なら
            DialogInterface.BUTTON_NEGATIVE->
                //ネガティブボタンなら
        }
    }
}

このように、ButtonがPositive or Negative or Neutralで判定されていて、どのPisitive Buttonなのかは関係がない。

呼び出し元で記述するコード

このままだとただAlertDialogを1つ作っただけで、まだいつ呼ばれるのかを定めていない。
呼び出すには、(詳細は参照だが)先ほど作ったAlertDialog本体のクラスのインスタンスを生成し、そのshow()メソッドを呼ぶ必要がある。
show()メソッドに渡す引数として、supportFragmentManagerと文字列があるが、これは次のを意味する。
(TODO:第9章読んだら書く)

dialogtest().show(supportFragmentManager, "dialogtest")

Activity(画面)

Androidの画面1つ1つはActivityで表現されている。1画面、1Activity、1XMLレイアウトファイル、1AppCompatActivityを継承したクラス(を含む1Koltinファイル)である。

Androidでの画面遷移は、現存するActivity Aの上に新しい遷移先のActivity Bを立ち上げて、その時のAの状態は一時保存される。戻るボタンでBが破棄され一時保存されたAに戻るというイメージ。

追加するときな手動でktファイルやXMLファイルを追加してもいいが、Android Studioの右クリック->New->Activity->Empty Activity(もちろんなんでもいいけど、何も入ってないのを基準にした)でやると便利。

package com.example.complexlistviewtest

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

class AddedActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        //既存のonCreate()にsaveInstanceStateを渡す ここ定型文
        super.onCreate(savedInstanceState)
        //Activityが持つlayoutのR値を与える。
        setContentView(R.layout.activity_added)
    }
}

Android公式の日本語Dialogチュートリアル
ダイアログ  |  Android デベロッパー  |  Android Developers

Activityのライフサイクル

今まで書いてきたonCreate()とかのより詳しい体系的な解説。参考文献の公式リファレンスであるが、以下のサイトがわかりやすい。
developer.android.com
もっとわかりやすくすると、図はこれ
f:id:Sen_comp:20200628021426j:plain

既存のonCreate()をoverrideして、新しいOverride()をAppCompatActivityを継承したクラス内で書く理由としては、このActivityへの動的操作もさながら、そもそもどのlayoutを表示するかを書く必要があるため。

Activityの呼び出し、削除

これらが実質Androidにおける画面遷移である。具体的な手順としては、以下のとおりである。
なお、Kotlinで作ったクラスはjavaに変換されてJVMで動かされるので、javaクラスなどを変数として渡す場合は、「クラス名::class.java」と書く。MainActivityクラスなら、「MainActivity::class.java」となる。

  • Intentというクラスのインスタンス(つまりIntent型の変数)を生成する。この時渡すのは(applicationContext, 起動するActivityのJavaクラス)である。
  • 追加でActivityに渡したい場合限定の操作。渡したいデータを適宜、さきほどのintentのオブジェクトのメソッドであるputExtra(String型のラベル, データ本体)で渡す。
  • startActivity()関数に先ほど作ったintentオブジェクトを渡す。

Activityを終了するのは簡単で、finish()をただ呼び出せばよい。Activityが1つしかないときにfinish()を呼ぶと、アプリ自体が強制終了する。
なお、intent.putExtra()で渡した追加情報はget〇〇Extra("ラベル名", val)でやる必要がある。(getStringExtra()だけはラベル名だけ渡せばよい) ここで、valは該当するラベルのデータがないときの代わりに入れるデータである。

詳細は以下のサンプルコード参照

//Activity A内でActivityBを起動しようとしてる
//Intentオブジェクトの宣言
var intent = Intent(applicationContext, ActivityB::class.java)
        
//適宜次のActivityに渡したいデータを用意する
//ないのなら飛ばしてよい
intent.putExtra("data_a", 334)
intent.putExtra("data_b", "9800")
        
//Activityを始める
startActivity(intent)

//ActivityBクラス内で、ExtraDataを利用したい場合

var a : Int = intent.getIntExtra("data_a", 123)
var b : String = intent.getStringExtra("data_b", "Yomiuri Giants Hara Tatsunori")

なお、標準型ではないようなデータを渡して受け取る場合は、(渡すのはputExtra()でOK)getParcelableExtra()などを使用することになる。
渡すときのデータの型は必ずParcelableインターフェースを継承しなければならない。

具体的には以下の記事にある。
Parcelableを使う話 - Qiita

オプションメニュー

f:id:Sen_comp:20200813012735p:plain
この右の縦に点が3つ並んでる部分は、押すと次のように展開される。
f:id:Sen_comp:20200813012822p:plain

このようなアクションバーに付属するメニューは、次のようにつけることができる。

1. menuフォルダが存在しない場合、resフォルダを右クリックし、[New]->[Android Resource Directory]を選び、出てくるダイアログのResource type:からmenuを選択してOKを選んで、メニュー関係のXMLを格納するフォルダを作る。
2. menuフォルダの右クリックから[New]->[Menu resource file]を選択して、メニューのXMLを作る。
3. MenuBarを出現させたいActivity内で、onCreateOptionsMenu()をoverrideする。

menuのXMLファイルについてだが、このような構造をしている。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/showingContextA"
        app:showAsAction="never"
        android:title="内容Aを表示"
        />
    <item
        android:id="@+id/showingContextB"
        app:showAsAction="never"
        android:title="内容Bを表示"
        />
</menu>

このように、menuタグでまず囲む。メニューを入れ子にしたい場合は、itemタグ内にmenu-itemタグを入れ子にすればできる。
app:showAsActionは、アクションバーに表示させるかどうかの設定である。属性値としては以下のような3つのものがある。

  • never   その選択肢はオーバーフローメニューに格納される(「...」の部分を押さないと見えない)
  • always   どんな状況であれアクションバーに表示する
  • ifRoom   アクションバーに表示するだけの空間があるのならば表示、そうでなければオーバーフローメニューに格納する

なお、app:showAsActionは、"http://schemas.android.com/apk/res-auto"から来てるため、わざわざ書く必要がある。ところで、Android Studioは賢いので、これを明示してなくても、メッセージが出てきて、Alt+Enterを押せば勝手にimportしてくれる。

Activity側のKotlinファイルでのコード

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    //inflate(作ったメニューXMLのR値, menu) 2つの目の引数は関数の引数そのまま
    menuInflater.inflate(R.menu.menubar_context, menu)
    return super.onCreateOptionsMenu(menu)
}

このようにonCreateOptionsMenu()をoverrideする。
inflateというのは膨らますという意味の英語で、具体的にどのメニューのXMLJavaのオブジェクトに膨らませる(実体化)させて使うのかをこのように指定する。
なお、return の部分はAndroid Studioが自動で補完をしてくれてるのだが、このように親クラスのonCreateOptionsMenu(menu)と呼びだして残りの作業をする必要がある。(これはおそらくメソッドをoverrideする際の定型文ともいうべき)

クリックされた時の挙動

このままではメニューを押すことができているが、押した時には何の挙動も得られない。
これは、Activity本体のクラス内で、onOptionsItemSelected()をoverrideすることで実現できる。具体的には以下のコードで説明する。

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    //itemは、選ばれた1つの選択肢を表す、メニューの選択肢の型の引数
    
    //このようにitemのitemIdを使って、メニューのデザインを規定したXMLファイル内の
    // どのVeiwのIDであるのかを判断し、それで分岐する。when文は相性が良い。
    when(item.itemId){
        R.id.showingContextA ->
            //do something
            R.id.showingContextB ->
            //do something
    }
    //これも最後に親クラスのoverride元の関数を呼び出して返す。
    return super.onOptionsItemSelected(item)
}
<以下自分用のメモ>
ListViewはAdapterが新しくセットされるたびに中身が変わる。これのonOptionsItemSelected()内でセットしなおせば、メニューボタンに応じてListViewの中身を変更することができる。これは(おそらく)ほかのViewでも同じ。
</以下自分のメモ>

メニューバーでの戻るキー

f:id:Sen_comp:20200812235258p:plain

多くのアプリには上のメニューバーには戻るボタンというのがある。これをつけるにはどうすればよいのだろうか?
これは、次の1行をつけたいActivityのonCreate()で追加するだけなのだ。

supportActionBar?.setDisplayHomeAsUpEnabled(true)

実を言うと、すでにAndroid側はこのような需要があるのを見越して、メニューバーにデフォルトでは表示させてないが、戻るボタンはすでに用意されてるのである!
なので、表示させるにはこのように、onCreate()で設定を変えてあげればよいだけである。
ただし、注意点として、supporActionBarはNull許容型変数であるため、メソッドや内部で定義されてる変数を呼び出すにはセーフコール演算子(.の前につけてる?)を使う必要がある。

これでは戻るボタンが出現するだけで、押した時の挙動は得られないので、さきほどのメニューと似たように、onOptionsItemSelected()内で挙動を定めればよい。

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    //メニューバーにおけるボタンが押された時の挙動はすべてこの関数で定義される
    //戻るボタンのID値は、android.R.id.homeである。
    if(item.itemId == android.R.id.home){
        //一般的には今のActivityをfinish()させるのが戻るボタンの仕事
        finish()
    }
    return super.onOptionsItemSelected(item)
}

コンテキストメニュー

次のようなメニューである。

f:id:Sen_comp:20200813005404p:plain

これを作るには以下のような手順を踏む必要がある。

1. メニューであるので、メニュー用のXMLレイアウトファイルを作成する。
2. 機能を搭載したいActivityのクラス内でonCreateContextMenu()をoverrideして、必要情報を追記する。
3. 機能を搭載したいActivityのクラス内でonCreate()の中で、必要情報を追記する。

具体的に説明していく。(1つ目はメニューバーと全く同じなので割愛)

onCreateContextMenu()のoverride

メニューバーにあるオプションメニューと似たように、onCreateContextMenu()をoverrideする。具体的な説明は以下のコードの通り

override fun onCreateContextMenu(
    menu: ContextMenu?,
    v: View?,
    menuInfo: ContextMenu.ContextMenuInfo?
) {
    //まず、親クラスのoverride先の関数をこのように呼び出す(Android Studioのテンプレにある)
    super.onCreateContextMenu(menu, v, menuInfo)
    //次に、メニューバーと同様、menuInflaterでメニューを実体化する
    menuInflater.inflate(R.menu.menubar_context, menu)
    //最後のタイトルについては、任意である。イメージ画像は上
    menu?.setHeaderTitle("ヘッダテキスト")
}

onCreate()の中の追記

このようにメニューをXMLレイアウトからjavaのオブジェクトに実体化したのならば、最後にはonCreate()の中で引数で渡したViewに対して、長押しを検出するregisterForContextMenu()を定める必要がある。

var targetListView = findViewById<ListView>(R.id.detailListView)
registerForContextMenu(targetListView)

クリックされた時の挙動

これもメニューバーの場合と似たように、onContextItemSelected()をoverrideして、その中で定めるということになる。詳しくはコードで

override fun onContextItemSelected(item: MenuItem): Boolean {
    //長押しされたViewに関する情報が格納されたオブジェクトを取得して、infoに格納
    val info = item.menuInfo as AdapterView.AdapterContextMenuInfo
    //長押しされたListでのポジション(0-idx)を取得して、listPostionに格納
    val listPosition = info.position
    //メニューバーと同様、Viewのidで場合分けする。
    //when文を使うと簡潔に実装できる。
    when(item.itemId){
        R.id.showingContextA ->
            //Do something
        R.id.showingContextB ->
            //DO something
    }
    //Android Studioで自動補完してくれるが、
    //override先の関数を最後に呼んでreturnするのを忘れずに
    return super.onContextItemSelected(item)
}

(TODO: この関数の中で押されてるListViewがどれなのかを特定する方法をReferenceから探す)

フラグメント(Fragment)

表示画面を数個のブロック分割し、それぞれのブロックをActivityを表示させる。つまり1画面に複数のActivityを表示できる。

f:id:Sen_comp:20200813014701j:plain

Fragmentは基本的にXMLで定義すれば最初からつけることができる。もちろん、動的にフラグメントを増やすこともできる。
動的に増やす場合、コードによって動的にどこのViewと置き換えるか、はたまた

具体的な操作は、次のようになる。
1. ActivityをいくつかのFragmentに分ける。Fragmentごとに必要となるレイアウトXMLとKotlinファイルを用意する。
2. 必要ならば適宜XMLファイルにfragmentタグで最初から存在するfragmentを定める。
3. コード内でフラグメントを新しく追加するときに、FragmentTransactionを利用する。Bundleという構造を使うことで、うまくFragment間のデータの受け渡しをこなすことができる。

Fragmentに分ける。

Activity->Fragment->Viewの順番でAndroidの部品は細かくなっていく。
FragmentはActivity内に何個でも詰めることができる、Activityのようなライフサイクルを持った、いくつかのViewを束ねるものである。Fragmentのライフサイクルは以下のとおりである。

f:id:Sen_comp:20200821015710j:plain

Activityのようなものであるが、Activityより下位の存在であるFragmentを定めるには、Activityと同様にレイアウトXMLと動的部分となるJava or Kotlinファイルが最低1つ必要である。
追加するには、New->Fragment->(何もないのが一番なら)Blankと選べばいい。Fragmentに詳しくないうちは下のチェック外したほうがよさそう(意味不明なのでわかったら追記する)
f:id:Sen_comp:20200821020009p:plain

Fragmentを構成するレイアウトファイルとKotlinファイルの一例を示す。

レイアウトファイル

<LinearLayout
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />
<!--普通のレイアウトファイルと全く同じ-->

Kotlinファイルはこのようになる

class UpperFragment : Fragment() {

    //このonCreateView()を継承することが最低でも必要である(自動生成なら勝手につけてくれる)
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // このFragmentをXMLとKotlinのクラスのデータから実体化させてViewとする。
        //viewとFragmentは内部的に包含関係にある?(わかったら書く)
        //最後にこのinflateされたviewを必ず返す。
        //デフォルトだとそのままreturnされるのでこう書き換える。
        val view = inflater.inflate(R.layout.fragment_upper, container, false)
        //操作をここに書く
        //findViewByIdは、FragmentはActivityのようにすでに実体を持ってる、というわけではないので?
        //inflateしたviewのメンバメソッドとして使う。
        view.findViewById<TextView>(R.id.sampleTextView)
        return view
    }
}

Fragmentを静的に追加する(最初からレイアウトに存在させる)

この場合は、追加したいレイアウトファイル(ActivityのものでもFragmentのものでも不問[要実験])に、次のようにTagを挿入すればよい。

<fragment
    android:name="com.example.fragmenttest.UpperFragment"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    />

nameとは、ここに埋め込むFragmentのクラス名であり、パッケージからのフル修飾名で書く。これは必須であり、抜けると強制終了とかの原因となる。(上の例は、com.example.fragmenttestパッケージのUpperFragmentクラス が管理するFragmentをここに埋め込むということ)
ほかにも当然、idやweightなどのタグもつけていいということになる。(動的に管理するのにidは必須なのでむしろ大体つけていく感じ)

Fragmentを動的に追加する

Kotlinファイル内でFragmentを追加するには、次の手順を踏む必要がある。
1. FragmentManager?.beginTransaction()でfragmentに関する一連の処理を管理するTransactionを作る。
2. 追加したいfragmentのインスタンスを生成する。
3. fragmentに含ませたいデータがある場合、Bundleという構造を利用して生成したFragmentのインスタンスに含ませたいデータを渡す。
4. Transactionに操作(fragmentを対象のcontainerに追加するadd, 指定のfragmentを削除するremove, containerに存在するのならばすべてのfragmentを削除して与えたfragmentに置き換えるreplaceなど)を行う
5. commit()を実行して、fragmentの変更をupdateする。

まず、Transactionという概念についてだが、一連の不可分の操作をまとめたようなものをTransactionと一般的に言う。今回のFragmentに関する処理では、FragmentManager?.beginTransaction()を使って変更手順を格納する変数を作り、一連の行われるFragmentに対する操作を記録して最後にcommit()で更新をする、という形になる。

Transactionの生成

呼び出すのはFragmentManagerと説明したが、Activityで呼び出すのかFragmentで呼び出すのかで実は呼び出すものが異なる。

  • activityで呼び出す場合は、supportFragmentManagerを使う。
  • fragmentで呼び出す場合は、fragmentManagerを使う。なおセーフコール演算子が必要
//Activity内
val transaction = supportFragmentManager.beginTransaction()
//Fragment内の場合 セーフコール演算子を使うこと
val transaction2 = fragmentManager?.beginTransaction()
追加したいFragmentの生成

これは単純に、追加したいFragmentのクラスのインスタンスを作るだけである。

val newFragment = AddItemFragment()
fragmentに含ませたいデータの追加

Activity間のデータの受け渡しではIntentを利用していたが、そのIntentも内部的にはBundleというデータを束ねた束を用いて実装されている。Fragment間のデータの受け渡しはIntentを使えないため、直接Bundleを介して使わなければならない。
もちろん、Intentの受け渡しでBundleオブジェクト自体を追加してActivity間のデータの受け渡しに使える。

Bundleの使い方としては、次のサンプルコードのようになる。

//初期化
val bundle = Bundle()
//このようにput〇〇でデータを渡せる。
//引数として1つ目はキー、2つ目はデータとなる
bundle.putString("china", "中華人民共和国")
bundle.putInt("china popuation", 1300000000)
        
//引き出しはこのようにする
bundle.getString("china")
bundle.getInt("china population")

bundleをfragmentに追加するのは、以下のようにargumentsに代入することとなる。

val newFragment = AddItemFragment()
val bundle = Bundle()
//bundleへのデータ追加
newFragment.arguments = bundle

追加したbundleは、Fragmentのクラス内では次のように取り出すことができる。

class AddItemFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_add_item, container, false)
        //argumensというBundleを格納した変数を使う。
        //argumenstに特に代入してないときは、nullであるかの判定をした方がよさそう。
        if(arguments != null) {
            val counter = arguments?.getInt("counter")
            view.findViewById<TextView>(R.id.showCountTV).setText(counter.toString() + "個目")
        }
        else {
            view.findViewById<TextView>(R.id.showCountTV).setText("0個目")
        }
        return view
    }
}
Transactionの操作

このままではFragmentのインスタンスをいくつか作っただけということになり、これをTransactionに一連の操作として追加していく必要がある。
ほかにも多くの操作があるが、下にある3つはかなり重要(他にも重要だと思うもの出てきたらその都度追記する)

  • add(対象のコンテナのR値, 追加するFragmentのインスタンス) コンテナにFragmentを追加していく。並び方はそのコンテナの指定による(Frame Layoutなら前のTransactionの上に重ね合わせ、Linear Layoutなら縦or横に並ぶ など イメージとしてはcontainerのタグの末尾にfragmentタグを次々に追加する)
  • remove(消したいFragmentのインスタンス) 指定したFragmentを削除する。消したいFragmentの院タンスを渡せばよい。自分自身ならthis(明示するならthis@クラス名)でいい。
  • replace(対象のコンテナのR値, 代わりに追加するFragment) 指定したコンテナ内のFragmentをすべて消して、第2引数のFragmentを追加する。

ほかにもoverloadされた形が様々にある。
具体的には次のようなソースコードとなる。

transaction.add(R.id.operationLinearLayout, newFragment)

//fragmentStackの一番上のFragmentを削除する例
transaction.remove(fragmentStack.peek())

transaction.replace(R.id.operationLinearLayout, newFragment)
commit()で更新をupdateする

変更点をすべて代入したら、最後にはcommit()を実行して変更をupdateする。

transaction.commit()

Switch

このようなON-OFFが切り替えられるViewのこと。
f:id:Sen_comp:20200828224450p:plain

これはボタンと似たように、Listenerを設定していく形式となる。大体な手順は以下の通り。
1. CompoundButton.OnCheckedChangeListenerを継承したListenerクラスを作成。ボタンのON-OFFが切り替わった直後にこの内容が実行される。
2. その中のonCheckedChanged()を必ずoverrideして、その中でON-OFFの切り替わった直後(つまり新しいON-OFF)になったときに施す操作を書く。
3. SwitchのViewのsetOnCheckedCangeListener()にListenerクラスをセットする。

SwitchはAndroidのライブラリの設計上、Toggle Button、ChechBoxとともにCompoundButton(合成ボタン?かな)クラス下のサブクラスである。よって、ボタンが切り替わった直後の内容はこれらの兄弟ボタンでも応用できる。
これらの兄弟などすべてに適用できるようなListenerであるため、「押されて内容が変わった直後」の状況を表すListenerはCompound.OnCheckedChangeListenerを使うこととなる。
具体的な使い方の一例

//onCreate()内
findViewById<Switch>(R.id.sw).setOnCheckedChangeListener(SwitchChangedListener())

//別途定義するListenerのクラス
private inner class SwitchChangedListener : CompoundButton.OnCheckedChangeListener{
    override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
        someBooleanValue = isChecked
    }
}

OnCheckedChangeListener自体は、onCheckedChanged(CompoundButton, Boolean)をoverrideすればよい。引数の意味としては、

  • 1つ目のCompoundButtonは変化が起きているView
  • 2つ目のBooleanは新しく変化したボタンの真偽値(押されてtrueになった、など)

参考資料
CompoundButton  |  Android Developers

参考文献
CompoundButton.OnCheckedChangeListener  |  Android Developers

音楽、動画の再生

TODO: 動画は未確認 やり方はあとで調べる

Androidでのネットワーク通信を介さない(つまりアプリに付随するデータとしての)音楽、動画再生はすべてMediaPlayerというクラスで一元的に管理されている。簡単な手順としては以下のようになる。

1. Projectのリソースにrawディレクトリを追加して、その中にアプリで使うような各種リソースを格納する。
2. その中に流したい動画or音楽を入れる。
3. 流したいActivityでMedia Playerを作成して、いくつかの初期設定を行う。
4. リソースを準備させる(マルチスレッド推奨)。
5. Media Playerのメソッドで提供された一時停止、再生、シーク機能、ループ再生などの機能が使用できる。
6. Media Playerの使用が終えたら、ActivityのonDestroy()必ずobjectをnullにする。

Projectへのリソースの追加

resディレクトリ以下にrawディレクトリを追加する。具体的には[New]->[Android Resource Directory]を選び、ウィザードでresource typeをrawとして選んで作成すればいい。
rawディレクトリでは加工されてない生のデータを格納するのに使う。つまり主なPNGなどの画像データ、音声データ、動画データもみんなここにおく。

(コラム)resディレクトリの下に置くものたち

以下の公式リファレンス(日本語)が詳しい。
App resources overview  |  Android Developers

Media Playerの初期設定

まず、Media PlayerオブジェクトはAndroidの中でもかなり重い部類なので、できるだけ「使う直前でオブジェクトを作成し、使い終わった直後に消す」を心がけるべき。消すといってもJavaベースのKotlinではガベージコレクションが働くので、オブジェクトへの参照を(基本的に)全て外せばいい。外すということなので、Media Playerへの参照を格納する変数自体にnullを代入するという形をとるため、宣言する際はMedia Player?のnull許容型で宣言することになる。

Media Playerは宣言時入り、新しくソースを追加したいときには以下のステップを踏む。

  • setDataSource()を使ってMedia Playerにデータを追加する。
  • setOnPreparedListener(), setOnCompletionListener()に、Media Resourceの準備終了時のやることのListenerと、Mediaの再生が終わったときのやることのListenerをセットする。
  • prepare() or prepareAsync()で準備させる。

まず、必須なのは1つ目のデータ追加と3つ目のprepare() or prepareAsync()の実行
データの追加に関しては、setDataResource()はoverrodeを含む8個の関数があるが、ここではActivityとURIのみ指定してする形式をとる。他はReference(English)参照。URLを指定できるのもある。

まず、データの場所を指すURIを用意する。
URIの規則としては、以下のようになる。

android.resource://アプリのルートパッケージ名/リソースファイルのR値
ルートパッケージTestApplicationであり、res/raw/music1.mp3を参照する場合は以下のようになる。
android.resource://TestApplication/${R.raw.music1}

一般的には一斉に変更できると便利なので、Kotlin特有の${}で文字列内に変数の内容を埋め込める機能を使って、

android.resource://${packageName}/${R.raw.music1}

と書く。

このように作成したURIをprase()を使用してMediaFileUriで格納して、これをsetDataSource(context, mediaFileUri)でセットするのである。詳細は以下。

//nullだった_playerにMediaPlayer()オブジェクトを1つ作りその参照を代入する。
_player = MediaPlayer()

//URIからデータを取り出し、parseしている。
val mediaFileUriStr = "android.resource://${packageName}/${R.raw.pengyou_english}"
val mediaFileUri = Uri.parse(mediaFileUriStr)

//このようにセットする。
_player?.setDataSource(applicationContext, mediaFileUri)

次の2つは任意であるがsetOnPreparedListener()とsetOnCompletionListener()である。なお、状態に関しては公式Referenceの完全状態遷移図(English)に準じ、それは2つ下の見出しにある。

setOnPreparedListener()はprepareAsync()が始まり、prepareAsync()で準備が終わって、prepared(待機状態)に移行する直前で行う操作を定義するListenerをセットする。そのListenerはMediaPlayer.OnPreparedListenerを継承して、virtualな部分を定義する形である(つまり今まで通りのAndroidのListenerの定義)。
具体的なソースコードは以下の通り

val _player : MediaPlayer?
_player = MediaPlayer()
//null許容型なので、セーフコール演算子?.を使用
//これはonCreate()やそれに準ずるところで書く。
_player?.setOnPreparedListener(PlayerPreparedListener())

//Listenerの定義。MediaPlayer.OnPreparedListenerを継承すること
private inner class PlayerPreparedListener : MediaPlayer.OnPreparedListener{
    override fun onPrepared(mp: MediaPlayer?) {
        //そのタイミングでやりたい操作をここに
   }
}

setOnCompletionListener()はStaredの状態で再生が終わって、isLoop == false(ループ設定をしてない)の時に、PlaybackCompletedの状態へ遷移の直前行う操作を定義するListenerをセットする。そのListenerはMediaPlayer.OnCompletionListenerを継承して、virtualな部分を定義する形である(つまり今まで通りのAndroidのListenerの定義)。
具体的なソースコードは以下の通り

val _player : MediaPlayer?
_player = MediaPlayer()
//null許容型なので、セーフコール演算子?.を使用
//これはonCreate()やそれに準ずるところで書く。
_player?.setOnCompletionListener(PlayerCompletionListener())

//Listenerの定義。MediaPlayer.OnCompletionListenerを継承すること
private inner class PlayerCompletionListener : MediaPlayer.OnCompletionListener{
    override fun onPrepared(mp: MediaPlayer?) {
        //そのタイミングでやりたい操作をここに
   }
}

Media Playerのprepare() or prepareAsync()

初期設定が終わったら、いよいよAndroidに準備させることとなる。このprepare()もしくはprepareAsync()のいずれかは呼ばれないといけない。
prepareAsync()は、メインスレッドとは別のスレッドを立ち上げ、そこでメインスレッドと非同期でprepare()をするものである。Media Playerのprepare()には時間がかかることがあるので、パフォーマンスのために非同期でやることが推奨されている。prepareAsync()は勝手にスレッド立ち上げまでやってくれる便利なメソッドである。
なお、両者にはマルチスレッド化されているか以外の違いはない。

Media Playerの各種機能

以下の図を見ましょう(終了)
f:id:Sen_comp:20200829203235p:plain
公式リファレンス(english)より引用。
MediaPlayer  |  Android Developers

MediaPlayerオブジェクトの破棄

Media PlayerはAndroidの部品の中でもリソースを多く使う方なので、使い終わり次第適宜解放することが必要である。一種の書き方としては、Activityが削除される直前でのOnDestroy()で、MediaPlayerのオブジェクトを指してる変数をnullとすることである。KotlinはJavaガーベジコレクション機能を持ってる(Javaコードに直して動かすので)ゆえに、基本的に削除したいObjectは何者からも参照されないようにすればよい。
以下サンプルコード

//nullの状態でMediaPlayerを格納した変数を宣言
var _player : MediaPlayer?

//なにかしらの代入をする。
_player = MediaPlayer()
...

//OnDestroy()内で、_playerのMediaPlayerの使用がすでに終わるのなら参照を消す。
_player = null

Service バックグラウンドで行われる処理

バックグラウンドで行われる処理については、Serviceというものを使うことになる。使うことで、アプリを切っても音楽を流し続ける、というようなことが可能になる。
主な流れは以下の通り。

1. Serviceクラスを継承したクラスを作成する。
2. Androidmanifest.xmlにサービスを登録する。
3. onStartCommand()をoverrideして、行う作業を記述
4. Activityからこのクラスを起動する。

ちなみに、Serviceにもライフサイクルがあり、それは次のようになる。
f:id:Sen_comp:20200831224641j:plain

Serviceクラスの継承とAndroidManifest.xmlへの登録

Android Studioのウィザードを利用すれば、1と2を勝手に行ってくれる。

Serviceを継承して作るクラスは、以下のようなものである。

class SoundManageService : Service() {
    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }
    override fun onStartCommand(){
        return Service.START_NOT_STICKY//このように適当な定数を返す必要がある。
    }
}

必ずoverrideしなければならないonBind()以外にも、onStartCommand()を定めることができる。

  • onBind()は、Serviceをバインドする状態と言い、(1つである必要はない)Activityと紐づけてお互いに情報を受け渡しできるという状態にする際に実行されるメソッドである。これは、紐づけたすべてのActivityが破棄されたらこのServiceも破棄される。
  • onStartCommand()は、Activity関係なく、いったん始まればstopService()を使わない限り一般的には止まらない。(電力がないなどの時はAndroid OSから強制終了されるらしい)

このうちの2つのうちのいずれかがServiceの形態によって実行されるという形となる。
なお、Service自体はイメージとしては描画能力がないActivityであり、ちゃんとライフサイクルも存在していて当然起動しているだけで電力を食う。Activityが消えてる時(起動はしているが今使っていない状態)でも裏で動作するイメージする。逆に、Activityが出てる時だけに裏で作業してほしい場合は、マルチスレッドを使用するのが一番である。

また、AndroidManifest.xmlには次のように追加記述する必要がある。

<service
    android:name=".SoundManageService">
    </service>

nameにServiceを継承したクラスの名前を登録しなければならない。
ほかにも次のようなオプションがある。

  • android:enabled 登録したサービスが利用可能か trueなら可能falseなら不可
  • android:exported 作成したサービスを外部のアプリが利用可能か。trueなら可能falseなら不可。

onStartCommand()での具体的操作

教科書にはonStartCommand()で始めるものしかないため、onBind()に関しては公式リファレンス張っておく(たぶん後で絶対使うのでその時にちゃんとやるしかない)。ここではonStartCommand()を使用する場合のやり方を書く。
onBind()はonStartCommand()でやる場合でも必要であるが、中身から or return nullでOK。

onStartCommand()内でやりたい作業を記述したのちに、最後に適宜な定数を返さなければならない。この下のうちの3つのいずれかを返すことになる。

START_NOT_STICKY //サービスが強制終了しても自動で再起動しない。
START_STICKY //サービスが自動終了しても自動で再起動。しかし渡されるIntent(後述)はnullとなる。
START_REDELIVER_INTENT //サービスが強制終了された時に自動で再起動するが、Intentは最後に渡されたものを引き継ぐ。
//処理を引き継いで再開するなら3つ目。

ActivityからのService開始と終了命令

Bindしない場合、開始と終了は次のようになる。

  • 任意のActivityで開始することができ、それはそのActivityが消滅してもServiceは続く。
  • Serviceを中止するには任意のActivityで中止命令を出すか、Service自身が中止をする以外にない。それらが無い限りServiceは永遠に動き続ける。(もちろんAndroid OSの介入で強制的に落とされることはあり得るが)

Service開始するには以下のように記述すればよい。Activity遷移とほぼ同じである。

val intent = Intent(applicationContext, SoundManageService::class.java)
//Activity遷移とほぼ同じである。
//必要に応じてputExtra()でデータの受け渡しができる。
//Activity遷移と違うのは、startActivity()ではなくstartService()である。
startService(intent)

Serviceの終了について、まずはService側(Serviceを継承したクラス側の)ないから自分自身を終わらせる(Activityでいうとfinish())方法を書く。以下のようにすればいい。

//Service内で自分自身を終了させるメソッド
stopSelf()

外部から終了させるには、Activity?内で次のように書けばよい。これもActivityの開始ととても似ている。

val intent = Intent(applicationContext, SoundManageService::class.java)
stopService(intent)

これらからわかるようにActivityとServiceは基本的に二重起動できない。(要調査)

通知機能

Android 8(Oreo) (Android API Level 26以降)で導入された通知Channelというクラスで通知を一元的に扱えるようになったので、その扱い方をもとに説明する。
通知の流れとしては、以下のようになる。
1. 通知Channelの生成
2. 通知ChannelのManagerへの登録
3. 通知を飛ばすときに、通知Channelを指定して、通知を作るBuilderを作成、設定する。
4. Builderで通知を飛ばす

通知Channelの生成

Android 8.0、API Level 26(Oreo)以降に導入された通知Channelは以下のようなことができる。これは開発者だけのみならず、ユーザー側が次のように通知をより細かくカテゴリー分けできるものである。
news.mynavi.jp

f:id:Sen_comp:20200902170439p:plainf:id:Sen_comp:20200902170802p:plain
f:id:Sen_comp:20200902170904p:plainf:id:Sen_comp:20200902170934p:plain

開発者側でも、通知Chennelを作り、Channelごとに重要度、音などの設定ができる。
この通知チャンネルを作るには次のようにする。

//id := これから登録するManager内部での識別文字列。アプリ内で被せてならない。
val id = "soundmanagerservice_notification_channel"
//これは上記のサービスサンプルのところに当たる。通知Channelの表示名
val name = getString(R.string.notification_channel_name)
//サービスの重要度 5つの定数のうちの1つを選ぶことになる。
val importance = NotificationManager.IMPORTANCE_DEFAULT
//これらをid, 名前, 重要度の順番でNotificationChannel()に渡せば作成できる。
val channel = NotificationChannel(id, name, importance)

通知Channelの重要度に関しては、以下のように5つの定数からなる。

IMPORTANCE_HIGH//重要度1位
IMPORTANCE_DEFAULT//重要度2位
IMPORTANCE_LOW//重要度3位
IMPORTANCE_MIN//重要度4位
IMPORTANCE_NONE//重要度5位

通知Channelの登録

このままでは通知Channel(ユーザーから言うと通知の緊急度、音などを設定したパラメータ)を作成しただけで、これをAndroidのOSに登録しなければならない。これは以下のコードによって行われいてる。

//Android OSレベルのサービスを取得する。渡してるのは内部的には文字列定数
//getSystemService()は様々の者を返すのでAnyを返すとなっている。
//今回はNotificationManagerを返してくるが、コード上は明示的にNotificationManagerにキャストしなければならない。
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//createNotificationChannel()に作ったChannelオブジェクトを渡して登録させる。
manager.createNotificationChannel(channel)

通知を飛ばすためのBuilder

このままでは通知の各種プロパティを規定したChannelを登録はしたものの、具体的な通知を飛ばすにはどうすればよいのかはノータッチであった。通知を具体的に飛ばすことは、NotificationCompat.Builderクラスによって実現できる。第1引数にコンテキスト、第2引数には登録したChannel固有の識別文字列を渡す。

//このようにBuilderに
val builder = NotificationCompat.Builder(applicationContext, "soundmanagerservice_notification_channel")

次に、この通知Channelに従った通知の内容(アイコン、タイトル、文章など)を定める。それは以下のようなコードとなる。

//アイコンをセットする。R値を渡すが、ここではAndroidに最初からついてるアイコンを使用
builder.setSmallIcon(android.R.drawable.ic_dialog_info)
//通知のタイトルをセットする。
builder.setContentTitle("再生開始”)
//通知の内容文章をセットする。
builder.setContentText("音声ファイルの再生を開始しました。")

ここまで設定が終わると、BuilderからNotificationオブジェクトが生成できる。

val notification = builder.build()

あとは、個の生成されたNotificationオブジェクトを、以下のようにSystemレベルから取得したNotificationManager(前の節で述べた)からnofity()を利用して通知を出せばよい。具体的には以下のコードとなる。

//先ほどと同じようにNotificationManagerをgetする。
manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//メンバメソッドのnotify()で通知を出すことができる。
//1つ目の引数は通知ごとの独立した番号 2つ目の引数はbuild()されたNotificationオブジェクト
manager.notify(0, notification)

NotificationManager.nofify()の引数に関しては、(2通りの取り方があってここではInt, Notificationだとする)1つ目は通知メッセージごとのアプリ内で一意的に決まるIntである。(なぜそうなのかは知らない。公式documentにもなかった)

これをつなげてかくとこうなる。

val builder = NotificationCompat.Builder(applicationContext, "soundmanagerservice_notification_channel")
builder.setSmallIcon(android.R.drawable.ic_dialog_info)
builder.setContentTitle(getString(R.string.msg_notification_title_finish))
builder.setContentText(getString(R.string.msg_notification_text_finish))
val notification = builder.build()
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(0, notification)

参考文献 
NotificationManagerの公式Reference(english)
NotificationManager  |  Android Developers
公式の(日本語の)通知の作り方
通知を作成する  |  Android デベロッパー  |  Android Developers


通知クリックによる遷移

このままでは通知は本当に通知してるだけである。押したらアプリが起動するようにするにはどうすればよいのだろうか?
具体的には以下のようなものになる。

1. 起動先にActivityを指定したIntentを作る
2. pendingIntentオブジェクトをそのintentから生成する。
3. 通知のbuilderにそのIntentをセットする。

Activityを指定してIntentを作成

これはActivityの画面遷移と全く同じように記述する。

//DestinationActivityへ遷移するintentを生成
val intent = Intent(applicationContext, DestinationActivity::class.java)

PendingIntentを生成

まず、PendingIntentとはなんであろうか?

普通のIntentは、いわば「メッセージを送信する相手に実行してもらいたい処理を記述したデータ」である。
出典:
メッセージングと通知 - mixi-inc/AndroidTraining

今まで使ってきた画面遷移でのIntent(上の節のあれ)は、applicationContext(つまり自分自身のcontext)から、DestinationActivityクラスへ(putExtra()したのならばそれらのデータも一緒に添えて)、意図(intent)と飛ばす。DestinationActivityクラスへ飛ばされた結果、新たにDestinationActivityが起動して画面遷移したように見える。

PendingIntent(保留するIntent)は基本的にはIntentと一緒だが、

  • Intentの送信タイミングを遅延する
  • 今操作してるアプリの外のアプリに、Intentの送信を行わせる。

ここでIntentの代わりに使用する。
今回の場合、通知をタップしたら、コーディングしているアプリではなく、「AndroidのOSの通知のシステム」という外部のものにActivity起動の意図(intent)を投げてもらうので、PendingIntentを使用することとなる。
この時、pendingIntentにはいくつもの生成方法があるが(Reference参照)、Intentから生成する方法をここで書く。

//StopServiceIntentはPendingIntentである。
//これは対象がActivityの場合の生成方法
val stopServiceIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)
//builderにsetContentIntent()でセットする。
builder.setContentIntent(stopServiceIntent)

対象がActivityの場合(それ以外の場合は以下の参考資料にある)、getActivity()を使うことになるが、それの引数は次のような意味を持つ。

  • Context (意味はあとで調べたい)
  • 識別番号 複数の画面部品からこのPendingIntentを利用する際に、区別するための番号
  • intent さきほどつくったもの
  • 重複処理 OS内に同じ種類のPendingIntentが残った場合、どんな処理をするかのフラグ

4つ目に関しては、以下の定数が取れる。

  • FLAG_CANCEL_CURRENT 既存のPendingIntentがあれば、それを破棄して新しいものを返す
  • FLAG_NO_CREATE 既存のPendingIntentがあればそれを使い、なければnullを返す(新しく作らない)
  • FLAG_ONE_SHOT 常に最初に作られたPendingIntentを返す
  • FLAG_UPDATE_CURRENT 既存のPendingIntentがあれば、それを破棄せずにExtraのデータだけ置き換える

公式のPendingIntentのreference(english)
PendingIntent  |  Android Developers

有志が作成したJavaAndroid開発の教科書のIntent部分 例が豊富
メッセージングと通知 - mixi-inc/AndroidTraining

機械翻訳日本語だけど質問回答
インテントとペンディングインテントの違い

PendingIntentの説明があるブログ。理解した後に見るとなるほどなぁという気持ちになる(個人差あり)
android初心者プログラミング: PendingIntent

暗黙的Intent

今までActivity起動のような、「どのActivity」を起動するのを指定してきた。(SomeActivity.class::javaを第2引数で渡していた)
しかし、たとえば地図、ブラウザのような別アプリに関わるものでは、

  • 正直どのブラウザを使うかわからない
  • 使うものが分かった(Google Chrome)としても、そのアプリのどのActivityに遷移すればいいのかわからない。

というわけでAndroidは、呼び出す側が「指定するおおまかな操作」と「そのデータのURI形式のもの」を与えたら、OS側が妥当なアプリを呼び出してそのアプリの妥当なActivityから開始してくれる、という暗黙的Intentが用意されている。

例えば、電話をかけたい、ブラウザでページを開きたい、メールを送信したいなどような「指定する大まかな操作」と妥当なURIデータを用意すればよいということになる。具体的なデータは以下の通り。

//URIデータを用意する
val uri = Uri.parse(uriStr)
//具体的に起動するActivityを指定しない暗黙的Intent
//第1引数は「指定する大まかな操作」 第2操作は適応させるURIデータ
//ここでは、VIEWという大まかな操作を指定している
val intent = Intent(Intent.ACTION_VIEW, uri)
//あとは明示的なIntentと同じようにstartActivity()するだけ
startActivity(intent)

ここではURIを渡しているが、URIが不要なACTIONも存在している。

以下が公式リファレンスの「指定するおおまかな操作」の一覧と簡単な用法である。
一般的なインテント  |  Android デベロッパー  |  Android Developers

startActivityForResult()

普通、明示暗黙問わず、Intentを渡してActivityを始めるときはstartActivity()を使用する。
しかし、Activity先の作業を完了した際に、元のActivityに戻って何かしらの処理を行う必要があるときにstartActivityForResult()を呼ぶ必要がある。コードは以下の通り

//第1引数はintent 第2引数はリクエストコード(意味は後述)
//いくつかのstartActivityForResult()をするなら、リクエストはそれぞれ違わないといけない。
startActivityForResult(intent, 200)

2つ目の引数のリクエストコードは、次の説明で分かる。

それとは別に、Activity内でonActivityResult()をoverrideする。これは、他のActivityを呼んでそのActivityの作業が終えて今のActivityにも戻った時に呼ばれるメソッドであり、Activityよんでそれ終えて今のActivityで(その結果に応じて)作業をしたい場合などに使う。
具体的な定義は以下のようになる。

public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    //ここに記述
}

引数の意味は以下のとおりである。

  • 第1引数requestCode Int さきほどのstartActivityForResult()の第2引数で渡したリクエストコード。
  • 第2引数resultCode Int 呼び出したActivityの結果が入る。IntだがRESULT_OK(処理が成功), RESULT_CANCELED(処理をキャンセル)の2値のみ与えられる
  • 第3引数data Intent 先ほどActivityのstartActivityForResult()で使用したIntent。この中のextrasを使ってデータを取り出す、などの操作が行える。

なので、requestCode別に条件分岐していくような記述になる。


Androidの実機の各機能を使用する際の許可

AndroidGPS,カメラ,写真などを利用するときに、以下のようにアクセスの許可を求められることになる。
f:id:Sen_comp:20200906023647p:plain

(出典 : 【Android】スマホ画面に「許可しますか?」と表示されたらどうすればいい? | スマホのいろは)

Android API level23以降は、AndroidManifest.xmlに記述するだけのみならず、許可が必要な処理を実行する前にユーザーから許可されているかどうかを判断する条件分岐文を記述しなければならない。
この許可の取り方は以下のようにする。

1. AndroidManifest.xmlに必要なコードを追記する。
2. そのような動作を行う前に条件分岐文でチェックを入れる。

AndroidManifest.xmlでの追記

AndroidManifest.xmlには、次のことをmanifestタグに含まれる、applicationタグの上に書く。(ずれても大丈夫かな?要検証)

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

ここではACCESS_FINE_LOCATIONという主にGPSなどで高精度の位置情報を取得する許可を取っている。
ほかの許可一覧(Android公式Reference(english))
Manifest.permission  |  Android Developers
これは次にのべるKotlin, Javaコードで記述する際にそれらのコードを使用するためのManifest.permissionクラスの中身の定数たちで記述されている。

Java,Kotlinサイドでの必要な記述

次に、許可が必要な情報(例えば詳細な位置情報)を使用する前に、条件分岐で許可をすでにとっているか?を記述する必要がある(Android Studioはそうしてくれと言ってくる)。これは、先ほどのManifest.permissionクラスなどを使用して次のように記述できる。

if(ActivityCompat.checkSelfPermission(applicationContext, Manifest.permission.ACCESS_FINE_LOCATION) !=
    PackageManager.PERMISSION_GRANTED){
    //ここに許可取ってない場合のコードを記述。
}

ActivityCompat.checkSelfPermission()関数の引数は以下のようになる。

  • コンテキスト。大体applicationContext
  • 判定する許可の文字列を、Manifest.permissionクラスの定数を使用して渡す。

これを渡せば、それぞれのデータの要求に対する結果を得ることができる。具体的には、以下のような2つの(定数として定義されている)結果になる。

PackageManager.PERMISSION_GRANTED//許可
PackageManager.DENIED//不許可

許可を求めるダイヤログ

最後に、許可を求めるダイヤログについて記述する。大体、さきほどのif文の判定例で、許可を頂けてない場合は許可が欲しいので、許可を求めるダイヤログでも出すだろう。これは、次のようにできる。

ActivityCompat.requestPermissions(this@MainActivity, ArrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1000)

引数はそれぞれ以下の意味を持つ。

  • Activity ダイヤログを出すActivity(基本的に出したいのはこの画面なので、まあ大丈夫だろう)
  • String 求めたい許可の文字列定数を配列化したもの。さきほどのManifest.permissionクラスの定数たちで、尋ねたい許可を配列に入れる(複数個訪ねたい許可があるときのために配列という形になっている)
  • Int リクエストコード。意味は後述

これだけではまだだめで、許可を取るダイヤログに対する処理を記述しなければならない。これはonRequestPermissionResult()をoverrideして記述することになる。これは以下のような骨子になる。

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
){
    //ここに記述する
}

引数の持つ意味は次のようになる。

  • Int リクエストコード。いくつの許可ダイヤログが生成されてもボタン押した瞬間、みなこの関数を呼び出すのでどの許可ダイヤログから呼ばれたのかを識別するリクエストコード
  • Array Manifest.permissionクラスの文字列定数の配列たち。リクエストコードと一致する許可ダイヤログで渡したものと同じ配列。
  • IntArray それぞれの許可に対しての結果。IntArrayだが返ってくる値は前述の2種類の定数。

ツールバー

こんなバー。静的な部分でもツールバーの説明があるのでそこも参照。
f:id:Sen_comp:20200907170712p:plain
静的部分も参照

ツールバーの動的な設定

以下のようになる。なお、Toolbarオブジェクト自体は2種類あるが、たぶんどれでもよさそう。(Android Level 29ならデフォルトでToolbarを扱える。)

//findViewByIdで取得
val toolbar = findViewById<Toolbar>(R.id.toolbar)
//ロゴ画像を設定
toolbar.setLogo(R.mipmap.ic_launcher)
//タイトル、タイトルの文字の色を設定
toolbar.setTitle(R.string.toolbar_title)
toolbar.setTitleTextColor(Color.WHITE)
//サブタイトル、サブタイトルの文字の色を設定
toolbar.setSubtitle(R.string.toolbar_subtitile)
toolbar.setSubtitleTextColor(Color.LTGRAY)
//最後にここまでの変更をupdateする
setActionBar(toolbar)

RecyclerView

ListVeiwを一般化したView。Android10での正式名称はandroidx.recyclerview.widget.RecyclerView。

以下のサイトがとても詳しい(javaであるが)のでこれを参照。以下の記事は部分的に補足していくだけ。
qiita.com

1つのRecyclerViewのために用意する必要があるのは以下の通り。

  • RecylerView#LayoutManagerの設定
  • RecyclerView#ViewHolderを継承してViewHolderを実装
  • RecyclerView#Adapter<さっき継承して作ったViewHolder>を継承して、Adapterを実装
  • RecylerView#LayoutManagerの設定

これはapp:layoutManager=""でXMLで静的に定めることもできる。

ListViewの場合は、リストのような表記しか選べなかったが(それはそう)、これはGrid Layoutのようなレイアウトも選択が可能である。
具体的には、以下のように記述する。

val lvMenu = findViewById<RecyclerView>(R.id.lvMenu)
//このように○○LayoutManagerのコンストラクタにContextを突っ込めんだものをlayoutManagerとして設定すればよい。
lvMenu.layoutManager = LinearLayoutManager(applicationContext)

//GridLayoutManagerは第2引数に一列の個数を定める リファレンス参照
//StaggeredGridLayoutManagerはリファレンス参照

選べる選択肢としては、
左からLinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager
f:id:Sen_comp:20200908233342p:plain -> f:id:Sen_comp:20200908233422p:plain -> f:id:Sen_comp:20200908234049p:plain
StaggeredGridLayoutManagerの場合、各行のアイテムのheightが異なってもそろえないが、GridLayoutManagerならばそろえる。

イメージや実装法として大いに参考になる記事
RecyclerViewを使ってみる - うさがにっき

ViewHolderの実装

ListViewにはなかったステップとして、各要素ユニットに含まれているView(R値ではない!)への参照を持つ、RecyclerView#ViewHolderを継承したViewHolderを実装しなければならない。
ユニットごとのレイアウトが以下のようならば、
f:id:Sen_comp:20200908235117p:plain
以下のようにViewHolderを実装する。

private inner class MyViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView){
    var tv1 : TextView
    var tv2 : TextView
    var tv3 : TextView
    init{
        tv1 = itemView.findViewById(R.id.tv1)
        tv2 = itemView.findViewById(R.id.tv2)
        tv3 = itemView.findViewById(R.id.tv3)
    }
}

動的に変更したいデータがあるViewは必ずHolderにそのViewへの参照を保持させないといけない。
逆に、ノータッチならば静的で定義した通りのデータになる。

Adapterの実装

ListViewではSimpleAdapterなどが存在していて、そのAdapterのフォーマットに従って妥当にやればよかったが(mutableMapなど)、RecylerViewでは自分でAdapterを実装しなければならない。これは、RecyclerView.Adapterを継承してAdapterを作ることになる。

まず、「RecylerViewに入れたいデータベースの型」だが、一般的には

  • ListViewのように1ユニットあたりのデータをmutableMapの形でデータベースが用意される
  • 独自のデータ格納クラスを作る

のうちのいずれかが取られる。

このAdapterでは3つ必ずoverrideしなければならない関数が存在していて、それぞれ以下のとおりである。

  • onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder
  • onBindViewHolder(holder: RecyclerListViewHolder, position: Int)
  • getItemCount(): Int
onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder

これは、各ユニットごとのViewHolderを生成ときに呼ばれる関数である。先ほど作ったViewHolderには、新たに1ユニット分のXMLから新しく生成(inflate)されたViewを渡す必要がある。渡されて、先ほど定義ViewHolderの中でfindViewById()で各Viewの参照を取る。
実装例は以下の通り。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder {
    val inflater = LayoutInflater.from(applicationContext)
        val view = LayoutInflater.from(applicationContext).inflate(R.layout.row, parent, false)
        return RecyclerListViewHolder(view)
    }
onBindViewHolder(holder: RecyclerListViewHolder, position: Int)

これは、用意されたHolderとRecyclerViewの番目数を使って、具体的にViewにデータを埋め込む関数である。
実装例は以下の通り。

override fun onBindViewHolder(holder: RecyclerListViewHolder, position: Int) {
    //_listDataはコンストラクタで渡されたデータベース
    //ここではMutableMapList<MutableMap<*, *>>形式
    val item = _listData[position]
    val tv1_content = item["name"] as String
    val tv2_content = item["price"] as String
    val tv3_content = item["unit"] as String
    holder.tv1.text = tv1_content
    holder.tv2.text = tv2_content
    holder.tv3.text = tv3_content
}
getItemCount(): Int

これは、RecyclerViewに含まれてるユニットの数を返す関数である。一般的には以下のように、データベースの配列の要素数と一致するはず(実装に寄るので何とも言えない)

override fun getItemCount(): Int {
    return _listData.size
}

ここまでくれば、あとは

lvMenu.adapter = RecyclerListAdapter(menuList)

でListViewと同じようにAdapterをセットすれば、無事RecyclerViewの動的変更が完了する。

区切り線の設定

このままでは区切り線が存在しないので、設定する場合は別途しなければならない。
これは、以下のように設定できる。

val lvMenu = findViewById<RecyclerView>(R.id.lvMenu)
val decorator = DividerItemDecoration(applicationContext, layout.orientation)
lvMenu.addItemDecoration(decorator)

基本的にRecyclerView#adItemDecoration()に、RecycerView.ItemDecorationクラスを継承したRecyclerViewへの修飾を記述したクラスを入れるが、区切り線ならばDividerItemDecorationというクラスとしてすでに用意されている。
DividerItemDecorationの引数は以下の通り

  • Context
  • 基本的にLayoutの向きと同じなので、LinearLayoutManagerのorientationプロパティを使う。

なお、orientationはLinearLayoutManagerのコンストラクタのうちの1種で定めることができる(リファレンス参照)

LinearLayoutManagerのreference(english)
LinearLayoutManager  |  Android Developers
自分でRecyclerView.ItemDecorationを継承して修飾するクラスを作る記事
RecyclerView.ItemDecorationについて #関モバ - Takuji->find;

Listenerの設定

ListViewでは、setOnItemClickListener()が用意されていたが、RecyclerViewは1つ1つのユニット(行)に対して、AdapterクラスのonCreateViewHolder()の中でinflateしたviewと結びつける必要がある。このさい、ButtonみたいにsetOnClickListener()である。具体的には以下の通り。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder {
    val inflater = LayoutInflater.from(applicationContext)
    val view = LayoutInflater.from(applicationContext).inflate(R.layout.row, parent, false)
    //このように、各ユニットを表すviewごとにsetOnClickListener()をする。
    //もちろんどこかでItemClickListenerクラスはView.OnClickListenerを継承して作られている。
    view.setOnClickListener(ItemClickListener())

    return RecyclerListViewHolder(view)
}

参考文献

野崎英一. やさしいKotlin入門. カットシステム. 2018/5/10

齋藤新三(著). 山本祥寛(監修). 基礎&応用力をしっかり育成!Androidアプリ開発の教科書 Kotlin対応 なんちゃって開発者にならないための実践ハンズオン Kindle版. 翔泳社. 2019/7/10

ViewBinderについて
mrstar-memo.hatenablog.com

多岐にわたり
developer.android.com

同じく多岐にわたり
Welcome to Android Development Training Course! - mixi-inc/AndroidTraining