Androidウィジェットで三目並べを作りたいけれど、RemoteViewsや更新の仕組み、タップイベントの受け取り方で迷って先に進めないことがあるはずです。
この記事を読むと、ウィジェット特有の制約を避けながら実際に動く三目並べウィジェットを作るための手順とコードが手に入ります。セットアップから複数インスタンス対応、ゲーム状態の保存、動作を軽くするコツまでやさしく丁寧に解説します。
| 項目 | 内容 |
|---|---|
| 実体験に基づく手順 | 段階的なコード例とデバッグでつまずきが少ないすすめ方を紹介します。 |
| ウィジェット固有のノウハウ | RemoteViewsやPendingIntentの扱い方、更新戦略をわかりやすく説明します。 |
| 応用と小ワザ | 複数インスタンス対応や状態保存、軽量アニメーション導入の実践的なアイデアを載せます。 |
まずは小さく動くものを作ってみると学びが早く楽しくなります。この記事の手順に沿って進めれば短時間で動くウィジェットが完成して、自信につながるはずです。
Android博士初めてでも大丈夫です。まずは最低限動くものを作ってから少しずつ改良していきましょう。詰まったら落ち着いてログとコードを見返すと解決の糸口が見つかります。
Androidウィジェットで三目並べを作る基本手順


ウィジェットで三目並べを作る基本は、表示と入力を分けて考えることです。RemoteViewsで盤面を描画し、タップ検出はPendingIntent経由でAppWidgetProviderが受け取りSharedPreferencesで盤面を管理します。ウィジェットはActivityと違って即時UI変更ができない点に注意してください。
実務的には二つの実装パターンがよく使われます。1つ目はRemoteViewsでセルタップを受けて即座に盤面を更新するシンプルパターン、2つ目は設定画面を用意してウィジェットごとに初期状態やオプションを保存するパターンです。どちらもSharedPreferencesのキー設計が肝になります。
ここでは両パターンの具体的な手順とプログラマー目線のコツを紹介します。特にSharedPreferencesへの保存タイミングとupdateAppWidgetの呼び方を実体験に基づいてわかりやすく示します。
RemoteViewsでセルをタップして手を置く実装パターン


RemoteViewsでセルタップを扱う場合は各セルに個別のPendingIntentを割り当てるのが基本です。ImageViewやTextViewを9個用意して、それぞれに異なるエクストラを持つIntentをセットします。
AppWidgetProviderは受け取ったインテントでSharedPreferencesを更新しupdateAppWidgetを呼んで描画を切り替えます。処理は短く保ちUI更新を最小限にするのが快適さにつながります。
res/layout/widget_tictactoe.xmlで3×3のセルレイアウトを作る
LinearLayoutかGridLayoutで3列構成を定義します。リサイズに備えてwrap_contentやmatch_parentを組み合わせておきます。
ImageViewやTextViewを9個配置し各セルに固有のandroid:idを付けます。RemoteViewsで画像やテキストを差し替えられるようにします。
contentDescriptionを入れdrawableは空・丸・バツなどstate別に用意しておきます。サイズはdpで統一します。
AppWidgetProviderでPendingIntentを作りセルタップを受け取りSharedPreferencesに保存する
各セル用のIntentにアクション名とwidgetId・セルインデックスをエクストラで入れます。受信側で識別しやすくします。
getBroadcastでPendingIntentを作り各セルのonClickに割り当てます。FLAG_UPDATE_CURRENTを使うと安全に上書きできます。
AppWidgetProviderがインテントを受け取りSharedPreferencesに盤面を保存してupdateAppWidgetでRemoteViewsを更新します。処理は短く保ちます。
設定画面付きで複数ウィジェットを管理するパターン


設定画面付きパターンはウィジェットごとに初期盤面やルールを変えたいときに役立ちます。ユーザーが追加時にオプションを選べるので使い勝手が良くなります。
Activityで受け取ったappWidgetIdをキーにSharedPreferencesへ保存し、結果を返す流れが基本です。キー命名をwidgetId単位で統一しておくと複数ウィジェットの管理が楽になります。
設定ActivityでウィジェットIDを受け取り初期盤面をSharedPreferencesに保存する
- appWidgetIdを受け取る。IntentからEXTRA_APPWIDGET_IDを取得し無効ならRESULT_CANCELEDで返します。
- 初期盤面を生成して保存する。空盤やランダム初期化などを選ばせてSharedPreferencesにwidgetId付きキーで保存します。
- 結果を返してウィジェットを更新する。ActivityはRESULT_OKとwidgetIdをセットしてfinishしウィジェットを反映させます。
onAppWidgetOptionsChangedとonDeletedでリサイズ対応とウィジェット削除時の後片付けを行う
- onAppWidgetOptionsChangedでサイズを受け取る。BundleのminWidthやminHeightを読み取りRemoteViewsのレイアウトや画像サイズを調整します。
- リサイズ時は部分更新を心がける。重い処理を行わず必要なビューだけを切り替えると滑らかに動作します。
- onDeletedで後片付けを行う。削除されたwidgetIdに対応するSharedPreferencesのデータを消去してメモリやストレージを解放します。
Androidウィジェットで三目並べを遊べるようにする応用


ウィジェットで三目並べを遊べるようにするには、見た目と動きだけでなく状態管理と更新の仕組みを整えることが大事です。ここでは実務で役立つ応用パターンをやさしく紹介します。
どの機能から先に取り組むか迷わないように、簡易AIの組み込み方法や非同期更新、画像切替と定期更新の組み合わせまで具体的に伝えます。まずは状態を確実に保存してから派生機能を足すと安心です。
- シングルプレイヤー用の簡易AIをGameクラスに組み込むパターン。勝ちや阻止の優先順位で素早く動くAIを実装できます。
- AI処理をWorkManagerや背景スレッドで非同期にしてウィジェット更新を安全に行う方法。UIスレッドをブロックしません。
- drawable資産を用意してRemoteViews.setImageViewResourceでセル画像を切り替える見た目改善の方法。リソース管理が楽になります。
- AlarmManagerやWorkManagerで定期更新を行い、ターン表示や簡易アニメーションを実現する方法。バッテリー負荷に配慮します。
シングルプレイヤー用の簡易AIを組み込むパターン


単純で実用的なAIはルールベースで十分です。勝てるなら勝つ手を優先し、相手に勝ち手があれば阻止する、取れるなら中央や角を選ぶといった優先順位を決めるだけで強く感じられます。
Gameクラス内に評価と手決定のロジックを入れておくとテストや保守がしやすくなります。まずは同期実行で動きを確認し、遅延が気になったら非同期化してウィジェット更新に繋げると安心です。
GameクラスにルールベースのAIを実装してプレイヤー後にAIの一手を決める
勝てる手→相手の勝ちを阻止→中央→角の順で候補を並べます。条件を明確にするとバグが減ります。
各セルを仮置きして勝敗判定を行う簡単な関数を用意します。合法手だけを評価することを忘れないでください。
プレイヤーの手を反映した状態でAIを呼び出し、決まった手で状態更新とウィジェット再描画を行います。
WorkManagerや背景スレッドでAI処理を非同期に実行してウィジェットを安全に更新する
- AI処理はCoroutineやExecutor、WorkManagerなどでメインスレッド外で実行します。処理時間が長いときも応答性を保てます。
- 結果をウィジェットに反映する際はAppWidgetManager.updateAppWidgetやBroadcastでメインスレッド側に受け渡します。RemoteViewsはメインで安全に扱ってください。
- 同時更新の衝突を避けるためにAtomicBooleanや同期処理で排他をかけます。競合による状態不整合を防ぐと安定します。
見た目を良くする画像と簡易アニメーションのパターン


見た目を良くすると遊ぶ楽しさが倍増します。drawableを使った静的なセル画像と、画像を切り替えるだけの簡単なアニメーションで十分に魅力的になります。
アニメは複雑にせずフレーム切替で代用するとバッテリー負荷が軽くて安心です。画像の命名規則と解像度をそろえておくと保守が楽になります。
drawable資産を用意してRemoteViews.setImageViewResourceでセル画像を切り替える
| 項目 | 内容 |
|---|---|
| 資産管理 | cell_x.pngやcell_o.pngのように用途がわかる命名をします。ベクターも使えますがウィジェットではPNGが安定します。 |
| 切替方法 | RemoteViews.setImageViewResourceでリソースIDを差し替えます。状態を保持してからまとめて更新するとチラつきが減ります。 |
| 推奨サイズ | 各セルはdp基準で用意し、複数解像度で用意すると見栄えが良くなります。重すぎないように最適化してください。 |
AlarmManagerまたはWorkManagerで定期更新を行いターン表示や簡易アニメを実現する
短いアニメやターン表示用にWorkManagerやAlarmManagerで周期的な更新を登録します。短時間の更新はバッテリーに配慮して控えめに設定してください。
RemoteViewsで表示を切り替え、短いフレーム差替えを繰り返すと簡易アニメが実現できます。テキストでターン表示を出すのも分かりやすいです。
アプリが不要になったらWorkManagerをキャンセルするかAlarmを解除してバッテリー消費を抑えます。状態に応じて有効化する運用が大事です。
よくある質問


- セルのクリックはどう受け取る?
RemoteViewsのsetOnClickPendingIntentでPendingIntentを設定し、Intentのextrasにセル番号やwidgetIdを入れてAppWidgetProviderで受け取ります。Android12以降はPendingIntentのフラグに注意してください。
- ゲームの状態はどこに保存する?
SharedPreferencesにwidgetIdをキーにして保存するのが手軽で扱いやすいです。更新のたびにAppWidgetManager.updateAppWidgetで該当widgetだけを書き換えてください。
- 頻繁に更新してバッテリーを食いませんか?
ユーザー操作があるときだけ更新するのが基本です。定期更新が本当に必要ならupdatePeriodMillisを避けてWorkManagerで条件を付けて行うと無駄が減ります。
- RemoteViewsの制約でUIが作りづらいときはどうする?
複雑な表現はBitmapで描画してsetImageViewBitmapで差し替えると見た目を工夫できます。あるいはウィジェットからアクティビティを起動して詳細画面で遊べるようにするのも実用的です。
- 複数のウィジェットを置くときの注意は?
各widgetIdごとに状態を分けて保存し、onUpdateやonDeletedでwidgetIdを意識して処理してください。onDeletedでデータを消すと不要なゴミが残らず安心です。
まとめ


ここまででAndroidウィジェットの全体像がつかめるはずです。ウィジェットはRemoteViewsで見た目を作りAppWidgetProviderで入力を受け取りクリックはPendingIntentで飛ばす流れになります。状態保存はSharedPreferencesが手軽でウィジェット更新はAppWidgetManager経由で行うと安定して動きます。
使いやすいコツとしてはRemoteViewsに重い処理や複雑なUIを詰め込まないことがおすすめです。複雑なやり取りが必要なら設定用のActivityに飛ばすと作りやすくなります。定期更新はWorkManagerやAlarmManagerでバッテリーに配慮しレイアウトは小さいサイズでも見やすいようにベクター画像と余白を工夫してください。
最後に遊び心を忘れずに作業してください。三目並べウィジェットならまずはローカル対戦や簡単なAIで動かしてから機能を増やすと学びが深まります。ログをこまめに出して問題を潰しつつさくっとテストして公開すると安心です。
