0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全く無知な状態から、Android用の動的リストを使うウィジェットアプリを作った

Last updated at Posted at 2025-03-07

はじめに

業務では主にSalesforceばかりやっており、アプリ開発なんて1mmも触れたことのなかった僕です。
ただ、どうしても自分用にアプリが欲しくなり「やれば出来るやろ!」の精神で頑張った足跡を残しつつ、今後同じようなアプリを作りたい人の参考になればと思います。

ただ、作り終わった後に記憶をなぞりつつ今書いているので、ヌケモレはあるかも・・。

出来たもの

こんな感じの、Googleカレンダーの内容を表示するウィジェットです。
ただ、公式で提供されているものと違う点として、「業務時間外(早朝、定時後、休日、有給日)であれば業務用アカウントの予定を表示しない」という機能をつけています。

【業務時間中】
Screenshot_2025-03-07-11-41-26-603_com.miui.home-edit.jpg

【業務時間外】
Screenshot_2025-03-07-12-33-38-424_com.miui.home.jpg

これは、休日に「明日の会議の予定」やら「明日の朝会の予定」やらなんやらを見てテンションを下げたくないからです。

製造過程

ながくなったのでCalendarContractに関しての情報は別記事にしました。

ざっくり各種フォルダ、ファイルの説明

ctrl+shift+nでファイル検索が出来ます。

AndroidManifest.xml

アプリの大本の設定をするところ、みたいです。
あとでちょこちょこ出てきます。

new_app_widget_info.xml

ウィジェット作成時に指定した名前によって、おそらく名前は変わるはず。
ウィジェットにまつわる設定をします。
再描画の間隔を指定したり、デフォルトサイズを指定したり。

MainActivity.kt

特に名前の設定をいじってなければ、おそらくこの名前になっているはず。
アプリを開いたときに動くコントローラーです。
ただ、今回作ったアプリは完全にウィジェット機能のみしか使ってないため、何も改修しませんでした。

NewAppWidget.kt

ウィジェット作成時に指定した名前によって、おそらく名前は変わるはず。
ウィジェットのコントローラーです。

NewWidgetService.kt

途中、自分で追加します。
ListView(動的リスト)を利用するために必要なクラスです。
この中で「どんなデータを表示するか」「各データにどのような値を表示するか」などを決めます。

layoutフォルダ

自分でデザインするようなレイアウトはここにxmlファイルを作成していくのがマナーのようです。
プロジェクト作成時にはありませんが、ウィジェット作成のタイミングで追加されました。

valuesフォルダ

デフォルトで「colors.xml」「strings.xml」「themes.xml」が入っていると思います。
ここに入っている内容は定数としてレイアウト画面やプログラムから参照出来ます。

文字色だとか、定型文だとかをここに保存していきます。

編集は直接xmlファイルを書き換えても出来ますし、画面左に表示されているResource Managerからでも出来ます。

1. Android Studioをインストール

ここはもう、他でさんざん記載があると思うので省略します。
インストールしました。

2. 枠組みを作る

初回だったらおそらく、新規プロジェクト作成のフローが自動的に表示される・・はず。

  1. どういったテンプレを使うか聞かれるのでEmpty Activityを選択
  2. 各種設定を行う
    Nameだけ好みのものに変更して、他はデフォルト値のままとしました。

3. ウィジェットを追加する

画像の通り、画面左のエクスプローラー(Android StudioだとProjectと呼ぶ?ややこし)の適当な箇所を左クリックし、Widgetを作成します。

ClassNameはわかりやすいものを適当に指定したら良いです。
今回はデフォルト値のままNewAppWidgetとしました。

そうすると、色々なファイルが追加されたり、AndroidManifest.xmlがいい感じに更新されたりしています。

4. ウィジェットに動的リストを追加する

ウィジェットはRemoteViewという仕組みを利用しているのですが、処理上の問題なのか、使えるレイアウトコンポーネントが限られています。
子アイテムをどのように表示したいかによってGridView ListView StackViewの選択肢があるのですが、今回はシンプルに縦並びにしたいので 「ListView」 を使います。

3.で自動生成されたnew_app_widget.xmlを開いて、PaletteからListViewをドラッグ・アンド・ドロップで突っ込みます。
この時、デフォルトで生成されている TextViewは残しておきましょう。
文字列やIDは変更しても良いですが、TextViewが後で一つ必要になります。

(上記はXMLを編集するやり方でも出来ます)

作成したウィジェットレイアウト用xml

色々いじってあるので、上の手順だけだと同じものにはならないです。
大事なのは

  • ListViewが追加されている
  • TextViewが存在している

こと!

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#99000000"
    android:orientation="vertical"
    android:theme="@style/Theme.CalendarWidget.AppWidgetContainer">

    <TextView
        android:id="@+id/updated_time"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginRight="7dp"
        android:text="TextView"
        android:textAlignment="viewEnd" />

    <ListView
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@null">

    </ListView>

    <TextView
        android:id="@+id/container_empty"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="予定がありません。"
        android:textColor="@color/TextColor"
        android:textSize="20sp" />

</LinearLayout>

5. 動的リストを制御する仕組みを作成する

今のところ動的リスト自体はレイアウトに作れましたが、そこに値を入れ込む仕組みがありません。
まずは制御するクラスを作成します。

フォルダ分けは自由にして良いですが、MainActivity.ktがあるあたりに新たにNewWidgetService.ktというクラスを作成します。

このクラスに必要なのは下記の2つ。

  • RemoteViewsServiceを継承したAppWidgetServiceクラス
  • RemoteViewsService.RemoteViewsFactoryを継承したAppRemoteViewsFactoryクラス(クラス名は自由)

です。

AppWidgetService

こいつは正直、とくに書くことはありません。
AppWidgetProviderを継承したクラスと、RemoteViewsFactoryを継承したクラスの橋渡しをしています。

class MyWidgetService : RemoteViewsService() {
    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
        return MyRemoteViewsFactory(this.applicationContext, intent)
    }
}

AppRemoteViewsFactory

こちらに動的リストのメインとなる処理を記載します。

WidgetItemクラス
class WidgetItem(
    public val day:String
    ,public val title:String?
    ,public val timeStr:String
    ,public val location:String
){

    public val description:String
        get() {
            var str = timeStr
            if (this.location != "") {
                str += "  ${this.location}"
            }

            return str
        }
}
class AppRemoteViewsFactory(
    private val context: Context
    ,private val intent: Intent
) : RemoteViewsService.RemoteViewsFactory {

    // 動的リストで表示するアイテム。別途WidgetItemクラスを作っています。
    private var widgetItems: MutableList<WidgetItem> = mutableListOf()

    // Widgetの初期読み込み時処理
    override fun onCreate() {
        // searchCalendarは別クラスにて用意した関数
        widgetItems.addAll(searchCalendar(context))
    }

    override fun onDestroy() {
        widgetItems.clear()
    }

    // ここの値分だけ、下のgetViewAtが呼び出される。
    override fun getCount(): Int {
        return widgetItems.size
    }

    // 動的リスト生成のメイン処理。
    // 動的リストのうち、position個目のアイテムの表示を作成している
    override fun getViewAt(position: Int): RemoteViews {

        val widgetItem = widgetItems[position]

        // 子アイテムに利用するレイアウト用xmlは分岐が出来る
        return if (widgetItem.day != "") {
            RemoteViews(context.packageName, R.layout.widget_item_head).apply {
                setTextViewText(R.id.item_day, widgetItem.day)
                setTextViewText(R.id.item_title, widgetItem.title)
                setTextViewText(R.id.item_description, widgetItem.description)
            }
        } else {
            RemoteViews(context.packageName, R.layout.widget_item).apply {
                setTextViewText(R.id.item_title, widgetItem.title)
                setTextViewText(R.id.item_description, widgetItem.description)
            }
        }
    }

    // 使ってないからわからん
    override fun getLoadingView(): RemoteViews? {
        // You can create a custom loading view (for instance when getViewAt() is slow.) If you
        // return null here, you will get the default loading view.
        return null
    }

    // 利用するレイアウト用xmlの数を返させる。
    // widget_item_headとwidget_itemの2つなので、2としている。
    override fun getViewTypeCount(): Int {
        return 2
    }

    // 使ってないからわからん
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    // 使ってないからわからん
    override fun hasStableIds(): Boolean {
        return true
    }

    // 一定時間ごとの再読込時処理
    override fun onDataSetChanged() {
        widgetItems.clear()
        widgetItems.addAll(searchCalendar(context))
    }
}

6. 動的リスト制御の仕組みを、ウィジェットのコントローラーに組み込む

5.で作ったクラスを利用する仕組みを組み込みます。

class NewAppWidget : AppWidgetProvider() {
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        for (widgetId in appWidgetIds) {
            // ListViewのデータ更新呼び出し
            // これを入れないと、再読込時にonDataSetChanged()が動かない
            appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.container)

            // RemoteViewsServiceを呼び出すIntentを作成
            // これを使って、5.で作った動的リストを制御する仕組みとコントローラーを紐づける
            val intent = Intent(context, AppWidgetService::class.java).apply {
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
                // Intentをユニークにするための処理
                data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
            }

            // この中でウィジェットに表示する値の設定やら、動的リストの設定をする
            val views = RemoteViews(context.packageName, R.layout.new_app_widget).apply {

                setTextViewText(R.id.updated_time, "最終更新:${DateTimeFormatter.ofPattern("HH:mm").format(LocalDateTime.now())}")

                // ListViewとRemoteViewsServiceを接続
                setRemoteAdapter(R.id.container, intent)

                // リストが空の場合のビューを設定
                setEmptyView(R.id.container, R.id.container_empty)
            }

            appWidgetManager.updateAppWidget(widgetId, views)
        }

        super.onUpdate(context, appWidgetManager, appWidgetIds)
    }
}

7. AndroidManifestを修正する

最後に、RemoteViewsServiceAndroidManifest.xmlに登録しないといけません。
こんな感じでreceiverの次辺りに追加します。

<receiver
    android:name=".NewAppWidget"
    android:exported="false">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>

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

<!-- RemoteViewsServiceを追加 -->
<service
    android:name="AppWidgetService"
    android:permission="android.permission.BIND_REMOTEVIEWS"
    android:exported="false"
/>

これでめでたく動く・・はず。

おわりに

ついに自分でアプリを作れるようになってしまった。
そこはかとない無敵感があります。

0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?