ラベル Kotlin の投稿を表示しています。 すべての投稿を表示
ラベル Kotlin の投稿を表示しています。 すべての投稿を表示

2025年5月5日月曜日

Android gradle.properties に定数を入れる

どうも。どっことです。今回はgradle.propertiesを使った設定値の管理方法について紹介します。

gradle.propertiesで設定値を管理する

gradle.propertiesを用意し定数値感覚で設定すれば、アプリのバージョンやtargetVersion/minSDKVersionなどの値を一つのファイルで管理させることができます。

設定方法

例えば以下のbuild.gradleにあるマジックナンバーを定数化しgradle.propertiesに移して参照しましょう。

android {
    namespace = "com.mkt120.sampleapplication"
    compileSdk = 35
    defaultConfig {
        applicationId = "com.mkt120.sampleapplication"
        minSdk = 26
        targetSdk = 35
        versionCode = 100
        versionName = "1.0.0"
        ... 
    }
    ... 

gradle.propertiesに外出しする定数を記述します。

ANDROID_SDK_VERSION=35
ANDROID_MIN_SDK_VERSION=26
ANDROID_VERSION_NAME="1.0.0"
ANDROID_VERSION_CODE=100

上で配置した定数をbuild.gradleで参照します。

android {
    namespace = "com.mkt120.sampleapplication"
    compileSdk = Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION)

    defaultConfig {
        applicationId = "com.mkt120.sampleapplication"
        minSdk = Integer.parseInt(project.ANDROID_MIN_SDK_VERSION)
        targetSdk = Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION)
        versionCode = Integer.parseInt(project.ANDROID_VERSION_CODE)
        versionName = project.ANDROID_VERSION_NAME
        ...        
    }
    ...

gradle.propertiesに記述した定数はproject.XXXXXの形式で参照することができます。

まとめ

今回はgradle.propertiesを使った設定値の管理方法について紹介しました。

参考

2025年5月4日日曜日

Android いまさら聞けないRelativeLayout

どうも。どっことです。今回はRelativeLayoutについて解説します。

RelativeLayout

RelativeLayoutはViewを相対的な位置関係で配置するためのViewGroupです。親のViewGroupを基準とした位置や同じRelativeLayout内の他Viewを基準として位置を決めることができ、FrameLayoutLinearLayoutより柔軟にViewを配置することができます。現在はConstraintLayoutが制約という形でRelativeLayoutよりも難しい配置設定を実現できるようになったことから、RelativeLayoutはレガシーなViewGroupという整理になっていますが、長く開発されてきたアプリの場合RelativeLayoutを使って画面レイアウトを実装しているものも多いと思いますので、理解しておくに越したことはないでしょう。今回はそんなRelativeLayoutの使い方を解説します。

押さえておくポイント

RelativeLayoutを使ううえで、押さえておくべきポイントは以下です。

  1. layout_alignParent***
  2. layout_center***
  3. layout_toStartOf
  4. layout_toEndOf
  5. layout_above
  6. layout_below
  7. layout_align***

順番に解説していきます。

layout_alignParent**

まずはlayout_alignParent**です。layout_alignParent** はViewを上下左右のいずれかに寄せる設定です。以下の4種類があります。

  • layout_alignParentTop
    • Viewを上寄せに配置する(デフォルトなので使うことが少ない)
  • layout_alignParentBottom
    • Viewを下寄せに配置する
  • layout_alignParentStart(Left):
    • Viewを左寄せに配置する(デフォルトなので使うことが少ない)
  • layout_alignParentEnd(Right):
    • Viewを右寄せに配置する

以下はサンプルです。layout_alignParentToplayout_alignParentStartは設定してもViewが移動しないので、省略します。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:text="alignParentEnd" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="alignParentBottom" />

</RelativeLayout>

こちらを実装した時のプレビューは以下です。

layout_center**

layout_center**は上下中央、左右中央、中央にViewを配置するための設定です。以下の3種類があります。

  • layout_centerVertical
    • Viewを上下中央に配置する
  • layout_centerHorizontal
    • Viewを左右中央に配置する
  • layout_centerInParent
    • Viewを中央に配置する

以下はサンプルです。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Center\nIn\nParent" />
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Center\nHorizontal" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:text="Center\nVertical" />

</RelativeLayout>

こちらを実装した時のプレビューが以下です。

layout_toStartOf

ここまでの配置設定は親Viewを基準にして設定するものでしたが、ここからは同じRelativeLayout内にある他のViewに対する設定となります。

まずはlayout_toStartOfです。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:text="target" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:layout_toStartOf="@id/text_view_target"
        android:text="toStartOf" />

</RelativeLayout>

layout_toStartOfを設定すると、指定したViewのと自身のの位置を揃えてくれます。

layout_toEndOf

次にlayout_toEndOfです。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_centerVertical="true"
        android:text="target" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/text_view_target"
        android:padding="8dp"
        android:text="toStartOf" />

</RelativeLayout>

layout_toEndOfを設定すると、指定したViewのと、自身のの位置を揃えてくれます。

layout_above

次にlayout_aboveです。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_centerVertical="true"
        android:text="target" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/text_view_target"
        android:padding="8dp"
        android:text="above" />

</RelativeLayout>

layout_aboveを設定すると、指定したViewのと自身のの位置を揃えてくれます。

layout_below

そしてlayout_belowです。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_centerVertical="true"
        android:text="target" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/text_view_target"
        android:padding="8dp"
        android:text="below" />

</RelativeLayout>

layout_belowを設定すると、指定したViewのと自身のの位置を揃えてくれます。

layout_align**

最後に紹介するのは、layout_align**です。この設定は指定のViewの端を揃えるものです。以下の4種類があります。

  • layout_alignTop
    • 指定Viewの上端を揃える
  • layout_alignBottom
    • 指定Viewの下端を揃える
  • layout_alignStart(Left):
    • 指定Viewの左端を揃える
  • layout_alignEnd(Right):
    • 指定Viewの右端を揃える

layout_alignTop

layout_alignTopです。指定したViewの上端と揃う位置に配置されます。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="target" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@id/text_view_target"
        android:padding="4dp"
        android:text="alignTop" />

</RelativeLayout>

指定したtargetと上端が揃う位置にViewが配置されます。

layout_alignBottom

次にlayout_alignBottomです。指定したViewの下端と揃う位置に配置されます。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="target" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/text_view_target"
        android:padding="4dp"
        android:textSize="8dp"
        android:text="alignBottom" />

</RelativeLayout>

指定したtargetと下端が揃う位置にViewが配置されます。(テキストサイズを小さくしています)

layout_alignStart

続いてlayout_alignStartです。指定したViewの左端と揃う位置に配置されます。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="target" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignStart="@id/text_view_target"
        android:padding="4dp"
        android:text="alignStart" />

</RelativeLayout>

layout_alignEnd

最後にlayout_alignEndです。指定したViewの右端と揃う位置に配置されます。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="target" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignEnd="@id/text_view_target"
        android:padding="4dp"
        android:text="alignEnd" />

</RelativeLayout>

まとめ

今回はRelativeLayoutについて解説しました。すでにレガシーとなったViewGroupですが、まだまだ利用されているプロジェクトも多いと思うので、それぞれの設定について理解しておきたいですね。

参考

2025年5月3日土曜日

Android いまさら聞けないLinearLayout

どうも。どっことです。今回はLinearLayoutについて解説します。

LinearLayout

LinearLayoutは順番にViewを並べてくれるというかなり便利なViewGroupであり、それでいて利用するために必要な設定が多くないところから今でも利用頻度が高いものとなっています。

今回はそんなLinearLayoutの使い方を解説します。

押さえておくポイント

LinearLayoutを使う上で押さえておくべきポイントは以下です。

  1. orientation
  2. layout_weight
  3. layout_gravity/gravity

順番に解説していきます。

orientation

orientationは、Viewをどの方向に並べるか設定するものです。Viewを順番に並べる機能を持つLinearLayoutの最も重要な設定となります。

設定は縦方向(vertical)か横方向(horizontal)のいずれかで、縦方向を設定した場合は上から下に、横方向を設定した場合は左から右にViewが配置されます。

もし画面に収まらない場合は当然はみ出してしまいますが、LinearLayoutにはスクロールする機能がないので、ScrollViewHorizontalScrollViewの中にLinearLayoutを配置することで画面をスクロール可能にしましょう。

layout_weight

layout_weightはLinearLayout内に並べたViewの大きさを比率で決定するためのものです。

例えばいくつかのViewを同じ大きさで並べたい場合は、すべてのViewのlayout_weightを1に設定することで全てのViewの大きさを1:1にすることができます。Viewの大きさを決めるlayout_widthまたはlayout_heightを0dpに設定する必要があります。

例えば3つのViewを均等幅(1:1:1)で並べる場合は以下です。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="左"
        android:background="@android:color/holo_red_dark"
        android:gravity="center" />

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="中"
        android:gravity="center" />

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="右"
        android:background="@android:color/holo_blue_dark"
        android:gravity="center" />
</LinearLayout>

このレイアウトのプレビューが以下です。

またlayout_weightの便利な使い方として、並べたViewのうちの1つだけ画面いっぱいに表示したいケースが挙げられます。

例えば画面内にヘッダ・フッタを持つページの場合です。ヘッダとフッタは固定(または可変長)、コンテンツ領域は残った領域全てとしてViewを配置するケースがほとんどだと思いますが、このようなケースでlayout_weightが大活躍します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/darker_gray"
        android:gravity="center"
        android:text="ヘッダ領域" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:text="コンテンツ領域" />


    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/darker_gray"
        android:gravity="center"
        android:text="フッタ領域" />
</LinearLayout>

この実装をプレビュー表示すると以下になります。

RelativeLayoutConstraintLayoutでも同様の画面構成を実現できますが、これらを使う場合はヘッダ・フッタ領域にidを付与する必要があり、それを基準にコンテンツ領域に対して制約をつける必要があります。

layout_gravity/gravity

layout_gravityはViewの位置を決める設定です。LinearLayout内のViewに対して設定します。

この設定はFrameLayoutにも同様にありますが、LinearLayoutではorientationの設定が反映されつつ、layout_gravityの設定が反映されます。

例えば、以下のケースを見てみましょう。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_red_dark"
        android:layout_gravity="end"
        android:text="左" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="中" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_dark"
        android:text="右" />
</LinearLayout>

このレイアウトのプレビューは以下になります。LinearLayoutのorientationの制約が守られつつ、layout_gravityに合わせた位置にViewが配置されています。

また、LinearLayoutに対してgravityを設定することもできます。gravityはView内の要素に対して位置を指定するもので、テキストの改行位置などを決めるためにTextViewでも指定できます。

↑の例にgravityを適用させるとどうなるか見てみましょう。LinearLayoutにgravity:centerを設定します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">
    <!-- 内部は同じなので省略 -->
</LinearLayout>

これのプレビューは以下になります。


全てのViewが中央寄せになりましたが、個別にlayout_gravityを設定しているは右寄せのまま、逆にもともと左寄せになっていたはLinearLayoutのgravityの設定により中央寄せに移動しました。

何も設定していなかったが中央寄せに移動するのは良いとして、が右寄せのままになっているのは、layout_gravity親のViewGroup(LinearLayout)のgravityの設定を上書きしているから、と考えられますね。

まとめ

今回はLinearLayoutについて解説しました。非常に高頻度に使われるとても使いやすいViewGroupなので、ぜひ使いこなせるようにしておきましょう。

参考

2025年4月29日火曜日

Android いまさら聞けないFrameLayout

どうも。どっことです。今回はFrameLayoutについて解説します。

FrameLayout

FrameLayoutはAndroidViewで使われるViewGroupの一つです。利用頻度はあまり多くありませんが、一時期まではかなり重要な使い道がありました。今回はそんなFrameLayoutの使い方について解説します。

押さえておくポイント

FrameLayoutを使うにあたって押さえておくべきポイントを解説します。

  1. layout_gravity
  2. 使いどころ

layout_gravity

layout_gravityはViewをどこに配置するかを設定するものです。LinearLayoutにもこの設定項目はありますが、LinearLayoutorientationの設定値にも影響する一方、FrameLayoutではこれ一つで配置が完結します。

layout_gravityには以下を設定することができます。|(パイプ)で繋げることで複数設定することも可能です。

  • top
    • FrameLayoutに対して上寄せにViewを配置します。
  • bottom
    • FrameLayoutに対して下寄せにViewを配置します。
  • start
    • FrameLayoutに対して左寄せにViewを配置します。
  • end
    • FrameLayoutに対して右寄せにViewを配置します。
  • center_vertical
    • FrameLayoutの上下中央にViewを配置します。
  • center_horizontal
    • FrameLayoutの左右中央にViewを配置します。
  • center
    • FrameLayoutの中央にViewを配置します。

実装サンプルを載せます。topstartはViewが移動しないので省略します。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:text="center"
        android:textSize="8dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"/>
        
    <TextView
        android:text="center\nvertical"
        android:textSize="8dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"/>

    <TextView
        android:text="center\nhorizontal"
        android:textSize="8dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

    <TextView
        android:text="end"
        android:textSize="8dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end"/>

    <TextView
        android:text="bottom"
        android:textSize="8dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"/>

</FrameLayout>

このレイアウトのプレビューが以下になります。

使いどころ

前述した通り、FrameLayoutは使いどころが少ないViewGroupです。ただ古いアプリの場合、特にAndroidのバージョンが4.4まではLinearLayoutRelativeLayoutforegroundの設定がサポートされていませんでした。その時に唯一foregroundがサポートされていたViewGroupがFrameLayoutでした。Viewをタップした時にオーバーレイする形でViewの色を変えたい時はこのforegroundが唯一の解決方法ですので、そのような表示をするためにFrameLayoutが利用されていたというわけです。

まとめ

今回はFrameLayoutの使い方について解説しました。「いまさら聞けないFrameLayout」と名打ちましたが、押さえておくべきポイントは他のViewGroupより少ないので、いざ使うことになったとしても困ることは多くないでしょう。

参考

2025年4月26日土曜日

Android ウィジェット機能の実装方法を解説

どうも。どっことです。今回はAndroidのウィジェット機能の実装方法について解説します。

ウィジェット機能

iOSと大きく違うところとして、Androidはウィジェット機能が挙げられると思います。アプリを起動しなくても、必要な機能だけ切り出したコンポーネントをホーム画面に配置することで、ユーザが必要なサービスにダイレクトにアクセスできるようになり、ユーザにより良い体験を提供することができます。

今回は、そんなウィジェット機能の実装方法について解説します。

実装手順

実装手順は以下の通りです。

  1. レイアウトファイルを用意する
  2. ウィジェット用の設定ファイルを追加する
  3. AppWidgetProviderのサブクラスを作成する
  4. AndroidManifest.xmlに追記する

順番に解説していきます。

レイアウトを用意する

後続の処理を円滑に進めるため、まずはウィジェットとして表示するレイアウトファイルを追加します。これは通常のレイアウトファイルと同様res/layoutフォルダに配置します。ここではwidget_sample_view.xmlとして以下を配置します。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:text="Button"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</FrameLayout>

ウィジェット用の設定ファイルを追加する

アプリがウィジェット機能を具備していることをOS側に検知してもらうため、設定ファイルを作成します。作成したファイルは(普段使うことがあまりない)res/xmlフォルダに配置します。ここではwidget_provider.xmlとして以下を配置します。

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget_sample_view"
    android:minHeight="100dp"
    android:minWidth="200dp"
    android:updatePeriodMillis="1000000" >
</appwidget-provider>

AppWidgetProviderのサブクラスを作成する

AppWidgetProviderを継承したカスタムクラスを作成します。今回はSampleWidgetProviderとして以下を作成します。

class SampleWidgetProvider : AppWidgetProvider() {

  override fun onUpdate(
    context: Context?,
    appWidgetManager: AppWidgetManager?,
    appWidgetIds: IntArray?
  ) {
    context ?: return
    val remoteViews = RemoteViews(context.packageName, R.layout.widget_sample_view)

    val widget = ComponentName(context, SampleWidgetProvider::class.java)
    appWidgetManager?.updateAppWidget(widget, remoteViews)
  }

}

AndroidManifest.xmlに追記する

AndroidManifest.xmlに前項までに追加した設定ファイルとクラスを追記します。receivertタグでSampleWidgetProviderを追加し、Widgetのアップデート通知を受け取るためのintent-filter、前項で作成した設定ファイルをmeta-dataとしてタグ内に設定します。

  <application
      ...>
      
      <receiver android:name=".SampleWidgetProvider"
          android:exported="true">
          <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
          </intent-filter>

          <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/widget_provider" />
      </receiver>

  </application>

ここまで実装すれば、実装したウィジェットをホーム画面に配置することができます。

補足

ウィジェット内のViewにクリックリスナーを設定したい

ウィジェット内にクリックリスナーを設定する場合、通常のクリックリスナーと異なり実装できることはIntentを発火させるだけになります。このIntentをActivityServiceBroadcastReceiverで受け取り、必要な処理をそちらで実施します。画面を表示したいならActivity、画面を表示せず必要な情報を更新したいなどであればServiceBroadcastReceiverが通知先になります。

以下の例は、SampleActivityを起動する場合(何らかの画面を表示するケース)です。RemoteViews.setPendingIntentを使って、クリックリスナーを設定したいViewのidと、そのViewがタップされたときに発火するIntent(PendingIntent)を渡します。

class SampleWidgetProvider : AppWidgetProvider() {

  override fun onUpdate(
    context: Context?,
    appWidgetManager: AppWidgetManager?,
    appWidgetIds: IntArray?
  ) {
    context ?: return
    val remoteViews = RemoteViews(context.packageName, R.layout.widget_sample_view)

    // ボタンがタップされた時のintent(PendingIntent)を設定
    remoteViews.setOnClickPendingIntent(R.id.button, createPendingIntent(context))
    val widget = ComponentName(context, SampleWidgetProvider::class.java)
    appWidgetManager?.updateAppWidget(widget, remoteViews)
  }

  private fun createPendingIntent(context: Context): PendingIntent {
    // SampleActivityを通知先として指定
    val intent = Intent(context, SampleActivity::class.java)  
    // 通知先がActivityなので、PendingIntent.getActivityでPendingIntentを生成。
    // requestCodeやflagは任意の値
    return PendingIntent.getActivity(context, 100, intent, PendingIntent.FLAG_IMMUTABLE)
  }
}

まとめ

今回はAndroidのウィジェット機能の実装方法について解説しました。開発したアプリを利用してもらうための導線としてもウィジェットは機能するので、利用する際は参考にしていただけると幸いです。

参考

2025年4月19日土曜日

Android そういえば Kotlin Coroutine ってどう使うのが良いの?

どうも。どっことです。今回はKotlinCoroutineのテーマです。

Kotlin Coroutine

Kotlin Coroutineは非同期処理などのコールバック地獄を解消してくれたり、可読性の高いコードを実装できるようになる便利な機能ですが、「実際のところ、どう使うのが良いの?」と考えました。今回はそんな使うと便利だけど実際どう使うかイマイチ分からない人向けに、ベストプラクティスのページの内容を簡単に紹介したいと思います。

TL;DR(結論)

ViewModelを実装してviewModelScope.launch/View側ではlifecycleScope.launchを使うのがよさそうです。

Google開発者サイトでコルーチンに関するベストプラクティスが紹介されています。コルーチンにフォーカスしたものだと、以下が挙げられています。

  1. Dispatcherを外から注入できるようにする
  2. ViewModelでコルーチンを作成する
  3. GlobalScopeは使わない
  4. コルーチンをキャンセルできるようにする

Dispatcherを外から注入できるようにする

withContextなどで引数に設定するDispatcher(どのスレッドで処理するかの指定)は、ハードコードせず外から注入できる形にした方が良いです。テストコードを実装するときにテストが容易にできるようになります。

// OK:処理するDispatcherを外から指定する
class SampleRepository(private val ioDispatcher: CoroutineDispatcher =  Dispatchers.IO) {
    suspend fun veryHeavyTask() = withContext(ioDispatcher) {
        // very heavy task
    }
}
class SampleRepository() {
    // NG: Dispatcherをハードコード(直接指定)しない
    suspend fun veryHeavyTask() = withContext(Dispatchers.IO) {
        // very heavy task
    }
}

ViewModelでコルーチンを作成する

ViewModelを作成し、そこでコルーチンを作成→非同期処理を実装するのがよいとのことです。

そして画面側に非同期処理が必要なことを意識させることがないよう、suspend関数として公開しないよう実装するのが良いとのことです。

class SampleViewModel(): ViewModel() {
    private val repository = SomeRepository()

    // NG:UI側にsuspend関数(=非同期処理)として公開しない
    suspend fun badSample() = repository.veryHeavyTask()

    // OK:ViewModel内でコルーチンを生成し非同期処理を実行する
    fun sample() {
        viewModelScope.launch {
            repository.veryHeavyTask()
        }
    }
}

GlobalScopeを使わない

テストコードの実装しにくさ、GlobalScopeをハードコードしていることによりDispatcherもハードコードされやすくなる点などから、GlobalScopeを利用しないことを推奨しています。

class SampleRepository() {
    fun veryHeavyTask() {
        // NG:GlobalScopeをハードコードしない
        GlobalScope.launch {
            // very heavy task
        }
    }
}

GlobalScopeの利用が正当であるケースはかなり稀で、アプリが生存期間中にアクティブな状態を維持しなければいけない状態とのことです。また、現在のスコープより長い生存期間が必要な処理であれば、CoroutineScopeを外部から指定するような構成とすることを検討すべきとのことです。

コルーチンをキャンセル可能にする

コルーチンはキャンセル可能ですが、実際コルーチン内でキャンセルされたか確認したり、停止したりするまではコルーチンはキャンセルされないとのことです。ensureActiveを使用することでキャンセルの確認ができます。

class SampleViewModel(): ViewModel() {
    private val repository = SampleRepository()

    fun sample(value: Int) {
        viewModelScope.launch { 
            val result = repository.validateValue(value)
            // キャンセルできるタイミングでアクティブか確認
            ensureActive()
            if (result) {
                repository.submit(value)
            }
        }
    }
}

まとめ

今回はKotlinCoroutineのベストプラクティスについて紹介しました。より詳しい内容は参考に記載したGoogleのベストプラクティスの記事からさまざまなページにアクセスできるので、ぜひ参考にしてください。

参考

2025年4月12日土曜日

Android BottomSheetDialogFragmentの実装方法を解説

どうも。どっことです。今回はBottomSheetDialogFragmentについて解説します。

BottomSheetDialogFragmentについて解説

BottomSheetDialogFragmentは通常のダイアログから派生したクラスで、下からスライドインで生えてくるような見え方が印象的です。最初に表示するダイアログの高さや、スクロールの制御もでき、通常のダイアログよりカスタマイズ性が高い点も特徴的です。

実装

最小構成で実装する場合は以下の手順です。

  1. BottomSheetDialogFragment用のstyleを追加する。
  2. BottomSheetDialogFragmentのサブクラスを実装する。

順番に解説していきます。

styleを追加する

<style name="BottomSheetDialogTheme" parent="@style/Theme.Design.Light.BottomSheetDialog"/>

BottomSheetDialogThemeというstyleを追加します。

サブクラスを実装する

BottomSheetDialogFragmentを継承したCustomBottomSheetDialogFragmentクラスを追加します。

class CustomBottomSheetDialogFragment :
    BottomSheetDialogFragment(R.layout.fragment_bottom_sheet_dialog) {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return BottomSheetDialog(requireContext(), R.style.BottomSheetDialogTheme)
    }
}

ダイアログ内で表示するためのコンテンツもレイアウトファイル(fragment_bottom_sheet_dialog.xml)として追加しておきましょう。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:gravity="center"
        android:padding="16dp"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hello_world!!"/>

</FrameLayout>

これだけ実装すれば、BottomSheetDialogFragmentを表示することができます。

class MainActivity : AppCompatActivity() {
  
  ...
  
  fun showDialog() {
   val dialog = CustomBottomSheetDialogFragment()
   dialog.show(supportFragmentManager, null)
  }
}

実際に表示すると、以下のようになります。

その他

ナビゲーションバーの表示を格好良くしたい

styleをカスタマイズします。

<style name="BottomSheetDialogTheme" parent="@style/Theme.Design.Light.BottomSheetDialog">
  <item name="android:windowIsFloating">false</item>
</style>

windowIsFloating:falseを設定することで、以下のようにナビゲーションバーの表示が不透明になり比較的自然な表示になります。

さらにダイアログとナビゲーションバーの表示を同じにしたい場合は、以下のようにBottomSheetというスタイルを追加し、BottomSheetDialogThemeのスタイルに追加してください。

<style name="BottomSheet" parent="@style/Widget.Design.BottomSheet.Modal">
    <item name="android:background">@android:color/darker_gray</item>
</style> <style name="BottomSheetDialogTheme" parent="@style/Theme.Design.Light.BottomSheetDialog"> <item name="android:windowIsFloating">false</item> <item name="android:navigationBarColor">@android:color/darker_gray</item> <item name="bottomSheetStyle">@style/BottomSheet</item> </style>

ダイアログの背景色を指定したい場合は BottomSheet スタイルの 背景色(background)を変更して下さい。このときBottomSheetDialogThemenavigationBarColorを同じ色になるように注意してください。

最初に表示される高さを調整したい

いくつか注意が必要です。

まず、BottomSheetDialogFragmentの呼び出し元のレイアウトがCoordinatorLayoutである必要があります。これは、高さ調整をするためのBottomSheetBehaviorクラスの制約となります。

ここではActivityがダイアログを表示するサンプルなので、Activityのレイアウトを調整します。

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ダイアログを表示"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>

次に、ダイアログが表示されるタイミングでBottomSheetBehaviorを生成しますが、この時に渡すViewをcom.google.android.material.R.id.design_bottom_sheetでfindViewByIdで参照する必要があります。これもBottomSheetBehaviorクラスの制約です。

class CustomBottomSheetDialogFragment :
    BottomSheetDialogFragment(R.layout.fragment_bottom_sheet_dialog) {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return BottomSheetDialog(requireContext(), R.style.BottomSheetDialogTheme)
            .also { dialog ->
                dialog.setOnShowListener {
                    dialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
                        ?.let {
                            BottomSheetBehavior.from(it).also {
                                // 後述
                            }
                        }
                }
            }
    }
}

Viewを参照するIDがSDK側のものなので、正直あまりいい実装とは言えないという所感です。より適切な実装がみつかりましたら、この記事を更新しようと思います。

ここまでを調整した上で、生成するBottomSheetBehaviorに対してpeekHeightstateを設定することで、最初に表示されるダイアログの高さを調整することができます。

class CustomBottomSheetDialogFragment :
    BottomSheetDialogFragment(R.layout.fragment_bottom_sheet_dialog) {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return BottomSheetDialog(requireContext(), R.style.BottomSheetDialogTheme)
            .also { dialog ->
                dialog.setOnShowListener {
                    dialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
                        ?.let {
                            BottomSheetBehavior.from(it).also {
                                it.peekHeight =
                                    resources.getDimensionPixelSize(R.dimen.bottom_sheet_dialog_initial_height)
                                it.state = BottomSheetBehavior.STATE_COLLAPSED
                            }
                        }
                }
            }
    }
}

peekHeightには最初に表示する高さを、stateにはBottomSheetBehavior.STATE_COLLAPSEDを指定してください。

まとめ

今回はBottomSheetDialogFragmentについて解説しました。通常のDialogFragmentより実装が必要なポイントが多いですが、見せ方のカスタマイズできる点や元の画面表示の邪魔をしない点では、ユーザに優しい表示方法の一つと言えます。使い所を意識して使いこなせるようになりたいですね。

参考

2025年4月6日日曜日

Android CustomTab(ChromeCustomTab)の使い方を紹介

どうも。どっことです。今回はCustomTab(ChromeCustomTab)の使い方を紹介します。

CustomTabについて

CustomTabはアプリ内で利用できるブラウザ機能です。CustomTabが登場する前はWebViewという機能がアプリでのブラウザとしての役目を果たしておりましたが、セキュリティや表示速度などの観点から現在はCustomTabを使うことが推奨されています。

実装

実装手順は以下の通りです。

  1. app/build.gradleに依存関係を追加する。
  2. CustomTabIntent.Builderを使ってCustomTabIntentを生成する。
  3. CustomTabIntent.launchUrlを実行する。

順番に解説していきます。

app/build.gradleを実装する

まずはapp/build.gradleに依存関係を追加します。

dependencies {
    // 既存にある dependencies に以下を追加する
    implementation "androidx.browser:browser:1.8.0"
}

CustomTabIntent.Builderを生成する

次にCustomTabIntent.Builderを使ってCustomTabIntentを生成します。

val builder = CustomTabsIntent.Builder()
val customTabIntent = builder.build()

CustomTabIntent.launchUrlを実装する

最後にCustomTabIntentにあるlaunchUrl関数を使って、アクセスしたいWebページを表示します。

customTabIntent.launchUrl(this@MainActivity, Uri.parse("https://www.google.co.jp/"))

その他

CustamTabの表示をカスタマイズしたい

CustomTabIntent.Builderに対して、パラメータを設定することでカスタマイズすることができます。今回は参考欄に詳細の項目のページを記載する程度で割愛。

認証に利用したい

現在2025/4時点ではまだalpha版ですが、AuthTabIntentが将来的に利用できるようです。

良くある認証フローでは、認証が完了したタイミングでアプリに戻るためのリダイレクトURLを発火してもらうことが一般的ですが、そのハンドリングの一部をAuthTabIntentで制御してくれるようです。

ChromeCustomTabから戻ってきたことを検知したい

activityResultLauncherを利用すれば実現できます。参考はこちら

ただ前述したように、CustomTabIntentlaunchUrlのインターフェースが実装されている点から、これは推奨される利用方法ではない印象です。

val intent = customTabIntent.intent.also {
    it.setData(Uri.parse("https://www.google.co.jp/"))
}
// registerForActivityResult で生成したactivityResultLauncherを使って呼び出し
launcher.launch(intent) 

まとめ

今回はCustomTabについて解説しました。WebViewよりも優れている点が多く、シンプルに使う分には簡単に実装できるためぜひ活用してみてください。

参考

2025年4月5日土曜日

Android Admobのためのライブラリ UMPの実装方法を解説

どうも。どっことです。今回はAdmobを利用するためのUMP(UserMessagingPlatform)ライブラリについて解説します。

UMPライブラリ

Androidアプリ内に広告を表示するためには、Admobライブラリを使って広告表示用のViewを組み込む必要がありますが、それと合わせてユーザに許諾を得る処理も必要です。

今回はユーザに許諾を得るためのUMP(UserMessagingPlatform)ライブラリの使い方について解説します。

実装概要

実装の手順は以下の通りです。

  1. app/build.gradledependenciesに依存関係を追加
  2. AndroidManifest.xmlmeta-dataを追加
  3. ユーザから許諾を得る処理を実装

順番に解説していきます。

依存関係の追加

app/build.gradledependenciesに依存関係を追加します。

dependencies { 
    ... 
    // 既存のdependenciesスコープに以下を追加
    implementation("com.google.android.ump:user-messaging-platform:2.0.0")
}

meta-dataの追加

AndroidManifest.xmlmeta-dataを追加します。valueに設定する値はAdmobのページから取得してください。

<!--Sample app ID: ca-app-pub-3940256099942544~3347511713-->
<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID"
           android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>

ユーザから許諾を得る処理を実装

ここからは広告を表示するにあたってユーザから許諾を得るための処理を実装します。

同意情報のオブジェクトを取得し更新する

UserMessagingPlatform.getConsentInformationconsentInformationを取得します。

取得したconsentInformationに対して同意情報を更新します。その後、処理終了が非同期に通知されます。

val params = ConsentRequestParameters.Builder()
    .setTagForUnderAgeOfConsent(false)
    .build()
val consentInformation = UserMessagingPlatform.getConsentInformation(activity)
consentInformation.requestConsentInfoUpdate(activity, params, {
    // 処理終了。後述。
}, { error ->
    // Handle the error.
})

表示が必要なフォーム(許諾情報)があるかチェックし、更新する

処理終了の通知を受けてフォームの利用可否をチェックし、可能であればフォーム更新処理に進みます。

val params = ConsentRequestParameters.Builder()
    .setTagForUnderAgeOfConsent(false)
    .build()
val consentInformation = UserMessagingPlatform.getConsentInformation(activity)
consentInformation.requestConsentInfoUpdate(activity, params, {
    // 処理終了。
    if (consentInformation.isConsentFormAvailable) {
        // 同意フォーム利用可能。
        // 更新処理へ。後述。
        loadForm(activity, consentInformation)
    } else {
        // 同意フォーム利用不可。
        // 設定、フォーム更新中など。
        // 再更新を促すのが無難。
    }
}, { error ->
    // Handle the error.
})

同意フォームを更新し、同意フォームを表示、ユーザに許諾を得る

UserMessagingPlatform.loadConsentFormで同意フォームを更新します。これはメインスレッドで実施するよう、サンプルにコメントされています。

更新処理が終了すると非同期に通知されるので、それを契機に許諾状態を確認しましょう。その結果、未許諾(REQUIRED)であればフォームオブジェクトに表示を依頼します。これにより、ユーザに同意フォームが表示されるはずです。

ユーザ操作を契機にフォームが非表示になったことがアプリに通知されますので、これを契機に許諾状態を再度取得してください。これでユーザに許諾してもらえていれば、晴れてアプリ内で広告表示可能という状態になります。

private fun loadForm(consentInformation: ConsentInformation) {
  // Loads a consent form. Must be called on the main thread.
  UserMessagingPlatform.loadConsentForm(activity, { consentForm ->
    if (consentInformation.consentStatus == ConsentInformation.ConsentStatus.REQUIRED) {
      // 同意フォームを表示しユーザに許諾を得る。
      consentForm.show(activity) {
        if (consentInformation.consentStatus == ConsentInformation.ConsentStatus.OBTAINED) {
          // 無事にユーザから許諾を得られた!
        } else {
          // 同意を得られなかった...
          // 同意をもらえなかったことによる制御へ
        }
      }
    }
  }, { error ->
    // Handle the error.
  })
}

まとめ

今回はAdmobを利用するための UMPライブラリについて解説しました。広告をアプリ内に組み込むためにはAdmobによる広告表示処理と合わせてユーザに同意を得る処理も必要不可欠ですので、参考にしていただけると幸いです。

参考

2025年1月4日土曜日

Android GridLayoutの使い方について解説

どうも。どっことです。今回はGridLayoutの使い方について解説します。

GridLayout

Androidのレイアウトを実装するとき、LinearLayoutやConstraintLayoutをよく利用しますが、Viewを格子状に配置する時はGridLayoutが便利です。 LinearLayoutを複数用意しなくても済みますし、ConstraintLayoutで必要なたくさんの属性値を設定しなくても済みます。また、一部のLayoutParam属性に初期値が設定されているので、実際に実装する時の設定項目が少なく済むのも良いところの一つかと思います。今回はそんなGridLayoutの使い方について解説します。

GridLayoutの使い方

今回は、レイアウトファイルでGridLayoutを利用する場合の実装を解説します。

<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:columnCount="3"
    android:background="@android:color/holo_red_light"
    android:rowCount="1">

    <TextView
        android:layout_row="0"
        android:layout_column="0"
        android:padding="10dp"
        android:background="@android:color/holo_blue_bright"
        android:text="00" />
    <TextView
        android:layout_row="0"
        android:layout_column="1"
        android:padding="10dp"
        android:background="@android:color/holo_blue_dark"
        android:text="01" />
    <TextView
        android:layout_row="0"
        android:layout_column="2"
        android:padding="10dp"
        android:background="@android:color/holo_green_light"
        android:text="02" />
</GridLayout>
  1. GridLayout
    1. layout_width:match_content指定。画面幅いっぱい指定。
    2. layout_height:wrap_content指定。必要最小限の高さ指定。
    3. layout_row:1、layout_column:3をそれぞれ指定。1行3列の格子指定。
  2. GridLayout内の子View
    1. layout_width/layout_heightを指定していない。
      • これはGridLayoutが初期値としてwrap_contentを設定しているため。
    2. layout_columnlayout_rowにはそれぞれの配置位置を指定。

この実装でそのままレイアウトを確認すると、以下が表示されます。

GridLayout内のTextViewが全て左寄せに表示されました。子Viewに大きさを指定していないので、当たり前といったら当たり前ですね。

LinearLayoutのlayout_weightのように、要素を均等に広げるためには、layout_columnWeightlayout_rowWeightを使います。今回は横幅を均等に広げるため、layout_columnWeightを使います。

<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/holo_red_light"
    android:columnCount="3"
    android:rowCount="1">

    <TextView
        android:layout_row="0"
        android:layout_column="0"
        android:layout_columnWeight="1"
        android:background="@android:color/holo_blue_bright"
        android:padding="10dp"
        android:text="00" />

    <TextView
        android:layout_row="0"
        android:layout_column="1"
        android:layout_columnWeight="1"
        android:background="@android:color/holo_blue_dark"
        android:padding="10dp"
        android:text="01" />

    <TextView
        android:layout_row="0"
        android:layout_column="2"
        android:layout_columnWeight="1"
        android:background="@android:color/holo_green_light"
        android:padding="10dp"
        android:text="02" />
</GridLayout>

これでレイアウトを表示すると以下になります。

均等幅に表示されました。

layout_rowWeightも同様です。上のケースは、GridLayoutの高さ(layout_height)がwrap_content指定になっているため、100dp指定に変更して試してみます。

まずは、何も指定せずに表示を確認します。

<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:background="@android:color/holo_red_light"
    android:columnCount="1"
    android:rowCount="2">

    <TextView
        android:layout_row="0"
        android:layout_column="0"
        android:background="@android:color/holo_blue_bright"
        android:padding="10dp"
        android:text="00" />

    <TextView
        android:layout_row="1"
        android:layout_column="0"
        android:background="@android:color/holo_green_light"
        android:padding="10dp"
        android:text="10" />

</GridLayout>

これでレイアウトを確認すると以下になります。

layout_columnWeightの時と同様、上寄せに要素が表示されました。layout_rowWeightを指定してみましょう。

<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:background="@android:color/holo_red_light"
    android:columnCount="1"
    android:rowCount="2">

    <TextView
        android:layout_row="0"
        android:layout_rowWeight="1"
        android:layout_column="0"
        android:background="@android:color/holo_blue_bright"
        android:padding="10dp"
        android:text="00" />

    <TextView
        android:layout_row="1"
        android:layout_rowWeight="1"
        android:layout_column="0"
        android:background="@android:color/holo_green_light"
        android:padding="10dp"
        android:text="10" />

</GridLayout>

これでレイアウトの表示確認をすると以下になります。

子Viewが均等な高さで表示されました。

まとめ

今回はGridLayoutの使い方について解説しました。LinearLayoutやConstraintLayoutで大抵のことは実現できてしまうところではありますが、実はGridLayoutもうまく使えば同じように便利に利用することができます。実はGridLayoutだと、LinearLayoutでよく困るアレを解決することができるのですが...これは別の記事で紹介したいと思います。

参考

2024年11月4日月曜日

Android 追加読み込みListViewを実装する方法を解説

どうも。どっことです。今回はListViewに追加読み込みする機能を実装する方法について解説します。

追加読み込み機能を持つListView

またまた、いにしえのListViewに関する解説です。RecyclerViewはカスタマイズ性に長けていますが、「単に一覧で表示してくれればいい」程度であれば、シンプル実装できるListViewもまだまだ使い道のあるコンポーネントかと思います。今回はそんなListViewで、追加読み込みをする機能の実装について解説しようと思います。

追加読み込みListViewを実装する

実装手順は以下の通りです。

  1. 読み込み表示Viewを実装する
  2. 読み込み表示Viewを表示する
  3. 読み込み処理タイミングを検知する

順番に解説していきます。

読み込み表示Viewを実装する

まずは読み込み表示用のViewを実装します。一般的にはプログレスを表示する形かと思いますので、そのViewを用意しておきます。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp">

    <ProgressBar
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

読み込み表示Viewを表示する

次に読み込み表示用Viewの表示処理を実装します。ListViewにはFooterViewとしてViewを追加することができるので、ここに読み込み表示Viewを設定します。

listView.addFooterView(createFooterView())
fun createFooterView(): View {
    return layoutInflater.inflate(R.layout.view_progress, null)
}

読み込み処理タイミングを検知する

読み込み処理を実装します。OnScrollListener#onScrollを使って、スクロールが最下部に来た時を検知し、読み込み処理を実施します。

listView.setOnScrollListener(object : OnScrollListener {
    override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {
        // ここでは気にしない
    }
    override fun onScroll(view: AbsListView?, firstVisibleItem: Int,visibleItemCount: Int, totalItemCount: Int) {
        if (totalItemCount == firstVisibleItem + visibleItemCount) {
            // todo:すべての要素が表示されたので、追加読み込みを処理する
        }
    }
}

onScrollStateChangedが気になる人は、判定処理をあらかじめ実装したinterfaceを定義して、そちらを渡すような実装としても良いかもしれません。

interface OnBottomScrolledListener : OnScrollListener {
    /**
     * 最下部までスクロールされたことを検知
     **/
    fun onScrolledToBottom()

    override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {
        // ここでは気にしない
    }
    override fun onScroll(view: AbsListView?, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
        if (totalItemCount == firstVisibleItem + visibleItemCount) {
            onScrolledToBottom()
        }
    }
}
listView.setOnScrollListener(object : OnBottomScrolledListener {
    override fun onScrolledToBottom() {
        // 最下部までスクロールされたことを検知したので、追加読み込みする
    }
}

また「読み込んだ結果、追加分が0件だった」場合に再度読み込み処理が実施されないよう、フラグなどでの管理も入れておきます。

listView.setOnScrollListener(object : OnBottomScrolledListener {
    override fun onScrolledToBottom() {
        if (!isLoadNecessary) {
            // 何もせず終了
            // このタイミングで読み込み表示Viewも消しておきましょう
            listView.addFooterView(null)
            return
        }
        // 最下部までスクロールされたことを検知したので、追加読み込みする
    }
}

まとめ

今回は下スクロールしたら追加読み込みしてくれるListViewについて解説しました。前回の解説でもコメントしましたが、RecyclerViewの登場によりListViewはもはやいにしえのコンポーネントとなっていますが、やはり初期装備が揃ってくれているおかげで実装が簡単という点がAndroidエンジニア初心者には優しいですね。

参考

2024年11月1日金曜日

Android 久しぶりにzxing-android-embedded を使ってみたので紹介

どうも。どっことです。今回はzxingを使ってみたので紹介します。

zxingを使ってQRコード/バーコードリーダーを実装する

一時期流行りましたよね。バーコードとか、 QRコードとか読み取って、いい感じになにかやること。ネイティブアプリで。そんな時に、今回使用したzxingというライブラリが非常に使いやすく、何度も私を助けてくれました。今回は、久しぶりにこのライブラリを使ってQRコード読み取り画面を実装してみたので紹介したいと思います。(最近はAndroid標準のカメラがいろいろ読み取ってくれるからもう出番ないんだよなぁ。。。

zxingの組み込みと実装の手順

ライブラリの追加(app/build.gradleに追記)

ライブラリを追加するので、例の如くapp/build.gradledependenciesに以下を追加します。

dependencies {
  implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
}

AndroidManifest.xmlに追記

今回はライブラリ側で「この制御をいれてね」という案内があるのでそれに従います。

<application android:hardwareAccelerated="true" ... >

実装

ハードウェア(カメラ)の制御や、読み取った時の解析はすべてzxing側でやってくれるので、アプリ側からは呼び出す箇所と、読み取り結果を受け取るところだけを実装すればOKです。こちらも例の如くActivityResultLauncherを使ってライブラリの画面を呼び出し・読み取り結果を制御します。

private final ActivityResultLauncher<ScanOptions> barcodeLauncher = registerForActivityResult(new ScanContract(),result -> {
    if(result.getContents() == null) {
        Toast.makeText(this, "Cancelled", Toast.LENGTH_LONG).show()
    } else {
        Toast.makeText(this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show()
    }
})
public void launch(View view) {
    barcodeLauncher.launch(new ScanOptions())
}

まとめ

今回はzxing-androidを使ってみたということで紹介しました。標準の機能が便利すぎるとはいえ、まだまだ使い所はあるかと思いますので、是非参考にしていただければと思います。

参考

2024年7月6日土曜日

Android テストコードの実装について解説

どうも。どっことです。今回は、テストコードの実装について解説したいと思います。

テストコード

テストコードは実機やエミュレータに開発段階のアプリをインストールせずとも、プログラムとしてテストを実行できるようにするものです。テストコードを実装せずとも、世の中に公開するアプリを開発することができるので、より難しい機能の実装や複雑なドメインロジックをプログラムに落とし込むなどのスキルを優先して獲得するというロードマップも考えとしてはあると思います。

ただ、昨今のCI/CDの重要性の高まりを鑑みると、テストコードを実装できることもエンジニアとして必要なスキルとして需要が上がってきているように見受けられます。そしてそんな需要が高まっている一方で、テストコードを実装できるエンジニアはまだまだ多くありません。ぜひテストコードを実装できるようになり、より広い知見をもつエンジニアへと一歩を進めましょう。

説明に入る前に(テストコードの種類)

Androidの開発においては、大きく2種類のテストコードがあります。

ユニットテスト

開発PC上で実行されるテストコードです。Androidの機能に依存しないビジネスロジックやアプリケーションロジックに対するテストコードを実装する際のものとなります。開発PC上で実行されるため、後述のインストルメントテストよりも高速に実行・実施ができます。

インストルメントテスト

実機やエミュレーター上で実行されるテストコードです。こちらは、Androidの機能に依存するUIまわりのテストや、外部機能などとの連携に対するテストコードとなります。より現実に近い環境でのテストとなるため、ユーザーストーリーを意識したテストに向いています。一方で、インストールなどのステップがビルド時に含まれるため、実行時間が比較的遅めになります。

今回は、ユニットテストの実装方法について解説します。

ユニットテストを実装する

ユニットテストの実装方法について、以下の流れで説明します。

  • ライブラリの準備
  • 実装
  • その他
    • Robolecticの紹介

ライブラリの準備

ユニットテストのテストコードを実装するためには以下をbuild.gradleに追加が必要です。プロジェクトを新規作成する際にAndroid Studioが自動で追加している場合がありますので、追加が必要か確認してください。

dependencies {
    testImplementation("junit:junit:4.13.2")
}

実装

それでは実装していきましょう。

テスト対象

テスト対象は以下のクラス・メソッドとします。

class Sample {
   fun test(input: Int?): Boolean {
       if (input == null) {
           throw IllegalArgumentException()
       }
       return input > 0
    }
}

テスト観点

テスト対象となるメソッドは、以下の観点でテストコードを実装することができます。

  • trueが返される確認テスト
  • falseが返される確認テスト
  • 例外が投げられるテスト

上をベースに確認するテストコードを追加していきましょう。

テストクラスを追加する

テストを実装するため、テストクラスを実装します。テスト対象のクラスで option + Enter からCreate Testを選択すればテストクラスを作成できます。SampleTestという名前にすると、testディレクトリに以下のようなクラスが作成できると思います。

class SampleTest {
}

trueが返される確認テストを実装する

まずはtrueが返されることのテストです。

@Test
fun testTrue() {
    val sample = Sample()
    val value = sample.test(1)
    Assert.assertEquals(value, true)
}

以降の実装も同様ですが、テストメソッドを実装し@Testアノテーションを付与します。

そして、実際にテストしたい内容を実装します。今回は、入力値に1以上を指定した場合はtrueが返却されるので、それを確認するためのテストを実装します。

falseが返される確認テストを実装する

次にfalseが返されることのテストです。

@Test
fun testFalse() {
    val sample = Sample()
    val value = sample.test(0)
    Assert.assertEquals(value, false)
}

同様に、falseが返却されるケースを実装します。入力値に0以下を指定した場合はfalseが返却されるので、それを確認するためのテストを実装します。

例外が投げられるテストを実装する

そして、例外が投げられることのテストです。

@Test(expected = IllegalArgumentException::class)
fun testException() {
    val sample = Sample()
    sample.test(null)
    Assert.fail()
}

例外が発生するテストは@Testアノテーションのパラメータexpectedを指定します。そのパラメータには発生する例外のクラスを指定します。

また、例外が発生しなかった場合がテストがNGとなるように、例外がthrowされる直後のタイミングでAssert.fail()を実装しておきます。

これにて、該当クラスメソッドのテストが実装できました。あとはテストを実行すれば期待した結果になるかを確認する処理が実行されます。

その他

ここまでテストコードの実装について解説しましたが、実は1点問題があります。ユニットテストは開発PC上で実行されますが、その都合により、AndroidSDKに含まれる機能を利用することができません。これが結構ネックになり、例えばよくある実装の一つであるFragmentのインスタンス生成メソッドは、Fragmentやそれに渡すBundleがAndroidSDKに含まれるものなので、テストコードを実装することができません。

ではどうするかというと、一つはインストルメントテストで実装することです。実機やエミュレーター上でテストすることで、テストコードを実行することができます。

もう一つはRobolectricを利用することです。RobolectricはユニットテストでもAndroidSDKに含まれる機能を利用することができます。

Robolectricの紹介

今回は説明を省略しますが、公式サイトを紹介します。またRobolectricの利用方法についても、後日解説を予定しています。

まとめ

今回はテストコード、特にユニットテストの実装方法について解説しました。テストコードやその実装の重要性は今後も高いものと予想されるので、今後もこれをテーマとした投稿をしていこうと考えていますので、よろしくお願いします。

参考

2024年6月30日日曜日

Android FlexboxLayoutの使い方を解説

どうも。どっことです。今回はFlexboxLayoutの使い方を解説します。

FlexboxLayoutを使う

タグやキーワードの一覧などに、Webサイトの見せ方の一つとして利用されている表示方法ですが、AndroidでもFlexboxLayoutというViewGroupで同様の表示が提供されています。今回はこちらのコンポーネントの使い方について解説します。

FlexboxLayoutを使ったレイアウトの実装方法

以下の順で利用方法について解説します。

  • ライブラリを追加する。
  • FlexboxLayoutをレイアウトとして利用する。
  • 表示確認

ライブラリを追加する

ライブラリを追加します。build.gradledependenciesに以下を追加します。

dependencies {
  implementation 'com.google.android.flexbox:flexbox:3.0.0'
}

FlexboxLayoutをレイアウトとして利用する

FlexboxLayoutをレイアウトとして利用します。LinearLayoutやConstraintLayoutと同様に、レイアウト内に組み込みます。

<com.google.android.flexbox.FlexboxLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:flexWrap="wrap">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="テキスト1"
        android:padding="16dp"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="テキスト2 少し長め"
        android:padding="16dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="テキスト3"
        android:padding="16dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="テキスト4 長くなったら折り返される"
        android:padding="16dp" />
            
</com.google.android.flexbox.FlexboxLayout>

このときFlexboxLayoutの属性にはapp:flexWrap="wrap"を設定してください。このとき、appのnamespaceが定義されていないせいで赤文字表示される場合はnamespace定義を追加してください。(ctrl + Enter で大体解決してくれる)

表示確認

上記で実装すると、以下のように表示されます。

長いテキストが設定されているTextViewが折り返された位置で表示されているのが見てわかると思います。

まとめ

今回はFlexboxLayoutの使い方について解説しました。

参考

2024年6月29日土曜日

Android RecyclerView コンテンツをドラッグ&ドロップする実装を解説

どうも。どっことです。今回はRecyclerViewを使ったコンテンツのドラッグ&ドロップの実装方法について解説します。

ドラッグ&ドロップ機能を持ったRecyclerView

RecyclerViewの一番の利点は、拡張性に富んでいることだと思います。RecyclerViewが登場する以前では、ListViewがシンプルにコンテンツ一覧を表示する機能を担っていました。実装難易度もそこまで高くなく、単に一覧表示をさせるものであれば、今でもこれで十分です。しかし、より便利に、より良いUXを提供することを考えるとListViewではもうまかないきれないです。RecyclerViewは、拡張しやすいコンポーネントとして出てきたもので、今回のような機能はまさにRecyclerViewが活きるパターンの一つだといえます。

RecyclerViewにドラッグ&ドロップを実装する

それでは実装手順を解説します。

  1. ItemTouchHelperインスタンスを実装する。
  2. ItemTouchHelperRecyclerViewにアタッチする。

順番に解説していきます。

ItemTouchHelperインスタンスを実装する

まずはItemTouchHelperを実装します。

ItemTouchHelperは、今回のドラッグ&ドロップをはじめとするユーザ操作を司るヘルパークラスとなっています。今回のドラッグ&ドロップでは、SimpleCallBack#onMove,SimpleCallBack#onMoved, SimpleCallBack#isLongPressDragEnabledのコールバックに追加実装が必要となってきます。

早速サンプルを載せます。

ItemTouchHelper(object : SimpleCallback(UP or DOWN, 0) {
    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        // 特定のコンテンツがドラッグされようとしている 
        val fromPosition = viewHolder.adapterPosition
        val toPosition = target.adapterPosition
        recyclerView.adapter?.notifyItemMoved(fromPosition, toPosition)
        // ドラッグしてOKなら true を, NGなら false を返す
        return true
    }

    override fun isLongPressDragEnabled(): Boolean {
        // 長押しされた。ドラッグ&ドロップを有効にする
        return true
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        // 呼ばれない
    }

    override fun onMoved(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        fromPos: Int,
        target: RecyclerView.ViewHolder,
        toPos: Int,
        x: Int,
        y: Int
    ) {
        super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
        // コンテンツがドラッグ&ドロップされた。
    }
})

まずSimpleCallBack#isLongPressDragEnabledでは、ユーザがコンテンツを長押しした時にドラッグ&ドロップを開始するかどうかを返します。trueなら開始する、falseならしないということになります。今回は簡単のため、trueを返すようにしています。

onMoveコールバックでは、ユーザのドラッグによりコンテンツをドラッグさせても問題ないかをチェックします。同じRecyclerView内に複数の種類のコンテンツが表示されている場合=並び替えをさせたくない場合はここでfalseを返すことで、そのドラッグ&ドロップを無効にすることができます。

そしてonMovedコールバックは、実際にコンテンツのドラッグ&ドロップが行われたタイミングでここの処理が呼び出されます。このタイミングで、コンテンツの要素を渡された引数の内容をもとに更新してあげるのがよいでしょう。

ItemTouchHelperをRecyclerViewにアタッチする

あとは実装したItemTouchHelperRecyclerViewにアタッチするだけです。Activity#onCreatedFragment#onViewCreatedあたりで以下を読んであげれば問題ありません。

itemTouchHelper.attachToRecyclerView(recyclerView)

まとめ

今回はRecyclerViewを使ったコンテンツのドラッグ&ドロップの実装方法について解説しました。RecyclerViewは拡張性が高く、今回以外の拡張も豊富に取り揃えているので、今後もテーマにしていきたいと思います。

参考

2024年6月23日日曜日

Android RecyclerViewを使ったコンテンツ一覧の実装方法を解説 その2

どうも。どっことです。今回はRecyclerViewをより実用的に利用する場合の実装方法について解説します。

RecyclerViewでのコンテンツ一覧のカスタマイズ

前回の解説では、RecyclerViewの必要最低限の実装について解説しました。しかしこれだけでは実用性に欠いており、より実用に近いケースが必要なことを課題として挙げていました。

今回は前回に挙げた課題、つまり以下の内容を解説していきたいと思います。

  • 表示要素を動的にする。
    • データベースやAPIから取得した情報を一覧に表示するようなケースを解説します。
  • クリックしたらその要素に対応する画面を表示したい。(クリックリスナーの制御)
    • RecyclerViewで表示しているコンテンツに対応するページに遷移させる実装を解説します。

それぞれ解説します。

RecyclerViewの表示要素を動的に制御する

実装の手順は以下の通りです。

  1. CustomAdapterのコンストラクタに表示要素のリストを追加する。
  2. getCount()メソッドを修正する。
  3. 表示要素の配列を使ってonBindViewHolder()でViewをカスタマイズする。

順番に解説していきます。

CustomAdapterのコンストラクタに表示要素のリストを追加する

CustomAdapterに、表示要素を管理するリストを渡します。今回はToDoリストを意識して以下のTodoクラスをあらかじめ用意します。

data class Todo(val id: Int, val title: String)

このデータのリストをCustomAdapterのコンストラクタで渡します。

class CustomAdapter(private val list: List<Todo>) : RecyclerView.Adapter<ViewHolder>()

getCount()メソッドを修正する

表示要素の数を設定します。表示要素は前項のリストの要素数と同じになるはずです。

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

表示要素の配列を使ってonBindViewHolder()でViewをカスタマイズする

ViewHolderに表示要素を反映していきます。onBindViewHolderでは表示要素の位置がパラメータとして参照できるのでそれを使って表示要素にアクセスします。

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    if (holder is CustomViewHolder) {
        val todo = list[position]
        holder.textView.text = todo.title
    }
}

クリックしたらその要素に対応する画面を表示する

実装手順は以下の通りです。

  1. タップ時のインターフェースを用意する。
  2. Viewがタップされた時の処理を追加する。
  3. CustomAdapter内でViewがタップされた時の処理を設定する。

こちらも順に解説します。

タップ時のインターフェースを用意する

Viewがタップされた時に、ActivityFragmentに必要な情報を渡すためのインターフェースを実装します。Todoクラスのデータを渡すように定義します。

interface ToDoClickListener {
    fun onClickToDo(todo: Todo)
}

このインターフェースをCustomAdapterのコンストラクタに追加します。

class CustomAdapter(
    private val list: List<Todo>,
    private val listener: ToDoClickListener
) : RecyclerView.Adapter<ViewHolder>() {

Viewがタップされた時の処理を追加する

Viewがタップされた時の実装をします。Todoデータへのアクセスが必要なので、この処理もonBindViewHolderに実装するとハマることなく実装できます。


override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    if (holder is CustomViewHolder) {
        val todo = list[position]
        holder.textView.text = todo.title
        holder.itemView.setOnClickListener {
            listener.onClickToDo(todo) // 要素がタップされたら、それに対応するTodoデータを渡す
        }
    }
}

CustomAdapter内でViewがタップされた時の処理を設定する

Viewがタップされた時に、インターフェースから渡された情報を使って必要な処理をするよう実装します。例えば、渡されたTodoデータを渡す形で、詳細画面に実装するなどでしょうか。インターフェースの実装はActivityFragmentに実装しても良いですし、実装した無名オブジェクトを渡しても問題ありません。今回は無名オブジェクトを渡します。


val adapter = CustomAdapter(listOf(
    Todo(0, "買い物"),
    Todo(1, "掃除"),
    Todo(2, "洗濯")
), object : ToDoClickListener {
    override fun onClickToDo(todo: Todo) {
        // 画面に反映、画面遷移など必要な処理を実装する
    }
})

まとめ

今回はRecyclerViewをより実用的に利用する場合の実装方法について解説しました。RecyclerViewによるコンテンツ表示はカスタマイズ性が高く、さまざまな利用パターンに当てはめられるので、今後もRecyclerViewによるコンテンツ表示について解説していきます。

2024年6月22日土曜日

Android 通知領域に通知を表示する方法を解説

どうも。どっことです。今回は、通知を実際に表示する実装方法を解説します。

通知領域に通知を表示する

通知チャンネルも実装できて、いよいよ通知を表示する方法まで来ました。さっそく実装方法を解説していきます。

通知を表示する実装の流れ

  • AndroidManifest.xmlにパーミッションの追加
  • 通知の表示の許諾確認
    • 拒否された時はさらに確認
  • 通知の表示処理

AndroidManifest.xmlにパーミッションの追加

まずはAndroidManifest.xmlに今回の通知表示に必要となるpermission.POST_NOTIFICATIONSの権限を追加します。このpermission.POST_NOTIFICATIONSが実装時に忘れやすいので、実装時にはぜひ備忘録としてご確認いただけると嬉しいです。

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

通知の表示の権限許諾確認

権限許諾ですが、実装方法は通常のパーミッションチェックと同様です。つまりregisterForActivityResultを使ってlauncherを生成し、Manifest.permission.POST_NOTIFICATIONSの許諾確認をします。このManifest.permission.POST_NOTIFICATIONSも実装時に忘れやすいので、ぜひ参考にしてください。

private val launcher = registerForActivityResult(
  ActivityResultContracts.RequestPermission()
) { 
  /** 許諾ダイアログが閉じられた。何かやりたいことがあればここに実装。*/ 
}

...

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    TODO("あとで追加実装")
    launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
}

拒否された時の再許諾確認

権限を確認したにもかかわらず拒否されてしまった場合ですが、一般的には次回などで「この権限がXXXという理由で必要なんだ」という権限を必要としている理由を説明した上で再度権限を確認する流れとなります。前項のTODO("あとで追加実装")に追記していきます。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
        // 権限を許諾してもらっている。後続処理に流さないでOK
        return
    }
    val shouldShow = shouldShowRequestPermissionRationale(
        Manifest.permission.POST_NOTIFICATIONS
    )
    if (shouldShow) {
        // 過去に許諾確認したけど、許可してくれなかった人。
        // ダイアログなどで権限を必要としている理由を説明した上で再度依頼する。
    } else {
        // それ以外。初回のケース、または「もう表示しない」設定としたケース。
        launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
    }
}

通知の表示処理

そしてManifest.permission.POST_NOTIFICATIONSの許諾を確認をした上で、問題なければNotificationを表示するようNotificationManagerに依頼します

if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
    val notification = Notification.Builder(this, "id")
        .setContentTitle("this is title")
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentText("this is text")
        .build()
    NotificationManagerCompat.from(this)
        .notify(null, 0, notification)
}

実際にうまく動作していれば、上記の場合以下のような通知が通知領域に表示されます。

まとめ

今回は通知領域に通知を表示する方法を解説しました。実装はそんなに複雑ではないものの、参照すべき定数を毎回忘れてしまいがちなので、ぜひ参考にしていただけると嬉しいです。

2024年6月17日月曜日

Android 通知チャンネルを作成する方法を解説

どうも。どっことです。今回は通知チャンネルを作成するための実装方法について解説します。

通知チャンネルを作成する方法

Android 8(Oreo)から 通知領域に通知を表示するためには、その通知に対応する通知チャンネルをあらかじめ作成しておかなければならなくなりました。

以前の記事で「作成した通知チャンネルの設定画面に遷移する」実装方法を解説しましたが、そもそも通知チャンネルの追加方法を解説していなかったので、今回はこちらをテーマに解説していこうと思います。

通知チャンネルの作成

通知チャンネルは以下の手順で作成します。

  1. NotificationManagerのインスタンスを取得
  2. 通知チャンネルインスタンスを生成
  3. NotificationManagerに通知チャンネルの作成を依頼

順番に解説していきます。

NotificationManagerのインスタンスを取得

まずはNotificationManagerのインスタンスを取得します。androidxではNotifcationManagerCompatというクラスが提供されており、今まではAndroidのバージョンがO未満かどうかで制御をする必要がありましたが、このクラスが提供されたことによりAndroidのバージョンを気にせず制御することができるようになりました。

呼び出し方も単純で、以下でOKです。

val manager = NotificationManagerCompat.from(this)

通知チャンネルインスタンスを生成

次に実際に作成する通知チャンネルのインスタンスを生成します。最小の構成では、ID、Importanceとタイトルさえあれば作成できるはずです。

val channel = NotificationChannelCompat.Builder("id", NotificationManagerCompat.IMPORTANCE_DEFAULT)
    .setName("ChannelName")
    .setDescription("ChannelDescription")
    .build()

実装するとき一番よく忘れるのが、Importanceです。定数はNotificationManagerCompatで定義されていますので、そちらを参照してください。以下が定義されています。

  • NotificationManagerCompat.IMPORTANCE_MAX
  • NotificationManagerCompat.IMPORTANCE_HIGH
  • NotificationManagerCompat.IMPORTANCE_DEFAULT
  • NotificationManagerCompat.IMPORTANCE_LOW
  • NotificationManagerCompat.IMPORTANCE_MIN
  • NotificationManagerCompat.IMPORTANCE_NONE
  • NotificationManagerCompat.IMPORTANCE_UNSPECIFIED

NotificationManagerに通知チャンネルの作成を依頼

最後に生成した通知チャンネルのインスタンスをNotificationManager#createNotificationChannel()で渡して、通知チャンネルの作成を依頼します。

manager.createNotificationChannel(channel)

まとめ

今回は通知チャンネルの作成手順を解説しました。Androidの通知に関する処理は、最新のバージョンになるに連れて必要な実装が増えていくのが辛いところですが、素敵なアプリには必要不可欠な要素の一つだと思いますので、是非ともマスターしたいところですね。

おまけ:通知チャンネルの削除を依頼する

作るのと同じで、NotificationManager#deleteNotificationChannel(id)を呼ぶことで、通知チャンネルの削除依頼することができます。

manager.deleteNotificationChannel(channel)

参考

移行予定

どうも。どっことです。 タイトルの通りですが、諸事情により GitHubPage に移行予定です。 https://mkt120.github.io/ この備忘録に記載の内容を転記しつつ、今後はこちらのページを更新していく予定です。