AndroidのローカルAIで読書ブログ投稿アプリを作ることにした 06(最終回)

2026年9月16日水曜日

Androidアプリ プログラミング

t f B! P L

読書ブログAI開発記・第6回(最終回)|1個のAPKにまとめて、アプリ完成

記事生成、Bloggerへの下書き作成、過去履歴、削除機能まで揃った。最後に残ったのは、完成したアプリを自分のスマホへ入れられる形で保存すること。Android Studioで動かすところから、1個のUniversal APKを生成するところまで仕上げた。
今回のサンプルについて
第5回までに完成したAndroidプロジェクトを、自分用のデバッグAPKへまとめる記事です。実際に生成へ成功した処理をもとに、説明用のコマンドとBATを掲載しています。個人のユーザー名・作業パス・認証情報は載せていません。

コマンドはWindowsのコマンドプロンプト向けです。今回の例はappモジュールのdebugビルドを前提にしています。

完成したので、APKとして残しておきたい

開発中はAndroid Studioから実行していた。しかし、自分用アプリとして使うなら、スマホへコピーしてインストールできるファイルを残しておきたい。

機能はもう十分。APKにして保存したい。
Android Studioでビルドできる。
でも、アプリ本体とは別にTensorのモジュールも表示されてる。
第2回で、Google Tensor用runtimeをDynamic Featureとして分けたからな。
あの構成が、最後にここで出てくるのか。

今回のプロジェクトには、アプリ本体とGoogle Tensor用runtimeの追加モジュールがある。別々のアプリを作っているのではなく、一つのアプリを構成する部品が分かれている状態だった。

Bundleを作れば、そのままスマホへ入るわけではなかった

これまで実行していたビルドは、gradlew.bat :app:bundleDebug。生成されるのはAndroid App Bundle、拡張子が.aabのファイルである。

今回欲しいのは、スマホへコピーしてタップできるAPKだった。そこで、Bundleからインストール用APKを生成する工程を加えることになった。

じゃあ、Bundlesで出力すれば終わり?
Bundleができたところで、もう一段ある。
自分用に残したいだけなんだけど。
その用途なら、今回のモジュールを含めたUniversal APKを作ろう。
1個になるなら、それがいい。

開発時の分割APKを使う方法もあるが、それぞれを別々にタップする使い方とは異なる。今回は保存して扱いやすい1個のファイルを作る方へ進めた。

似た拡張子を、先に整理する

今回の処理で出てくるファイルは3種類ある。名前が似ているので、完成したあとに迷わないよう整理しておく。

拡張子今回の役割スマホへ渡すもの
.aabGradleで作るAndroid App Bundle。APK生成の材料直接タップしてインストールするものではない
.apksbundletoolが生成するAPKセットの入れ物今回は中からAPKを取り出す
.apk取り出したUniversal APK今回インストールに使う完成ファイル

bundletool build-apks--mode=universalを指定すると、1個のAPKを含むAPKセットを生成できる。機能モジュールをそのAPKに含める条件として、Manifestのdist:fusingも確認する。Android公式:bundletool

Tensor runtimeを含められる設定にする

まず、Tensor用Dynamic FeatureのManifestを調整した。今回のパッチでは、インストール時に組み込む設定と、Universal APKへ含めるための設定を整えた。

そのうえで、BundleからAPKを生成するbundletoolを使う。最終的な生成処理はWindows用のbuild_universal_apk.batへまとめた。

AIを動かすコードも書き直すの?
今回変えるのは、完成物へどうまとめるかという設定と生成手順だ。
写真や記事の処理は、そのままでいいのね。
それまでに作ったアプリとTensor runtimeを、1個のAPKとして出力する。

Universal APKにしたからといって、すべてのAndroid端末で今回のTensor向けAI構成が使えるという意味ではない。ここで目指したのは、自分の端末向けに作ったアプリを1個のインストール用ファイルへまとめることだった。

変更するのは、Tensor用モジュールのManifest

今回のプロジェクトでは、次のファイルを調整した。これはプロジェクトからの相対パスである。

litert_npu_runtime_libraries\google_tensor_runtime\src\main\AndroidManifest.xml

既存の<dist:module>内で、配信設定とfusing設定を次の形にする。Manifest全体の置き換えではなく、該当部分の例だ。既存のモジュール名・タイトル・application設定などは保持する。

<dist:delivery>
    <dist:install-time>
        <dist:removable dist:value="false" />
    </dist:install-time>
</dist:delivery>

<dist:fusing dist:include="true" />

distは、Manifestのルートでxmlns:dist="http://schemas.android.com/apk/distribution"として宣言されている名前空間である。既に同じ要素がある場合は、重ねて追加せず既存の値を変更する。

removable=falseはインストール時モジュールを分割APK生成時に本体へ統合する設定、fusing=trueはUniversal APKへ含めるためにも使う設定である。今回のパッチでは両方を整えた。Android公式:Feature ModuleのManifest

removableをfalseにするだけで、Universal APKになる?
それだけではない。fusingの設定も確認して、最後にbundletoolでUniversal APKを生成する。Manifestの設定と生成コマンドが一組だ。

まずは3段階のコマンドで確認する

使うのは、既存プロジェクトのGradle Wrapper、ビルドに使えるJDK、bundletool、ZIPから取り出すためのPython 3。javapythonはコマンドプロンプトから実行できる状態にしておく。

今回の成功時に使ったbundletoolは1.18.3だった。以下のサンプルもその版を例にする。「常に最新版」という意味ではなく、今回使った版の記録である。bundletool公式配布ページからJARを用意し、プロジェクト直下へ置く。

まず、gradlew.batがあるプロジェクト直下でBundleを作る。

gradlew.bat :app:bundleDebug

今回の構成ではapp\build\outputs\bundle\debug\app-debug.aabができる。次に、それをUniversal APKを含むAPKセットへ変換する。

if not exist "apk_output" mkdir "apk_output"
java -jar "bundletool-all-1.18.3.jar" build-apks ^
  --bundle="app\build\outputs\bundle\debug\app-debug.aab" ^
  --output="apk_output\BookBlogAI-debug.apks" ^
  --mode=universal --overwrite

最後に、.apksの中のuniversal.apkを取り出す。次のコマンドは1行で実行する。

python -c "from pathlib import Path; import zipfile; z=zipfile.ZipFile(r'apk_output\BookBlogAI-debug.apks'); Path(r'apk_output\BookBlogAI-debug-universal.apk').write_bytes(z.read('universal.apk')); z.close()"

これで、使いたい名前のAPKがapk_outputへ出力される。コマンドプロンプトの複数行例では、行末の^が継続を表す。

最後のBATでも、少しつまずいた

APK生成用のBATを実行すると、最初はbundletoolが見つからずに停止した。さらに、日本語のメッセージがWindowsのコマンドプロンプトで文字化けし、一部がコマンドとして解釈される問題も起きた。

アプリが完成したと思ったら、今度はBATでエラー。
bundletoolの準備と、BATの文字コードの問題だな。
アプリより、最後の梱包で困ってるんだけど。
BATのメッセージを英数字に揃えて、必要なJARの取得処理も入れ直した。

その後は、ダウンロード後の変数を確認する処理にも修正が必要になった。取得に失敗したように見える表示でも、実際には確認側の変数展開に問題があった。

ここまで来ると、AIそのものとは関係のないWindows側の仕上げである。最終的には生成処理が最後まで通るようになった。

普段はBATを実行するだけにする

毎回3段階のコマンドを打たなくて済むよう、生成処理をBATへまとめる。実際の完成版にはbundletoolの自動取得も加えたが、記事のサンプルはJARを先に置く形に絞った。ビルド・APK生成・抽出の流れは同じである。

以下をbuild_universal_apk.batとしてプロジェクト直下に保存する。本文は英数字だけにし、Windows用の改行(CRLF)で保存する。

@echo off
setlocal EnableExtensions DisableDelayedExpansion
cd /d "%~dp0"

if not exist "gradlew.bat" (
    echo ERROR: gradlew.bat was not found.
    exit /b 1
)
where java >nul 2>nul
if errorlevel 1 (
    echo ERROR: java was not found in PATH.
    exit /b 1
)
where python >nul 2>nul
if errorlevel 1 (
    echo ERROR: python was not found in PATH.
    exit /b 1
)

set "BUNDLETOOL=%CD%\bundletool-all-1.18.3.jar"
if not exist "%BUNDLETOOL%" (
    echo ERROR: Place bundletool-all-1.18.3.jar in this folder.
    exit /b 1
)

set "AAB=%CD%\app\build\outputs\bundle\debug\app-debug.aab"
set "OUTDIR=%CD%\apk_output"
set "APKS=%OUTDIR%\BookBlogAI-debug.apks"
set "APK=%OUTDIR%\BookBlogAI-debug-universal.apk"
set "TMPAPK=%OUTDIR%\BookBlogAI-debug-universal.tmp.apk"

if not exist "%OUTDIR%" mkdir "%OUTDIR%"
if not exist "%OUTDIR%" goto :failed

echo [1/3] Building the Debug App Bundle...
call gradlew.bat :app:bundleDebug
if errorlevel 1 goto :failed
if not exist "%AAB%" goto :failed

echo [2/3] Generating one Universal APK...
java -jar "%BUNDLETOOL%" build-apks --bundle="%AAB%" --output="%APKS%" --mode=universal --overwrite
if errorlevel 1 goto :failed

echo [3/3] Extracting universal.apk...
python -c "import sys,zipfile; from pathlib import Path; z=zipfile.ZipFile(sys.argv[1]); Path(sys.argv[2]).write_bytes(z.read('universal.apk')); z.close()" "%APKS%" "%TMPAPK%"
if errorlevel 1 goto :failed
if not exist "%TMPAPK%" goto :failed
move /y "%TMPAPK%" "%APK%" >nul
if errorlevel 1 goto :failed

echo SUCCESS: Universal APK created.
echo "%APK%"
exit /b 0

:failed
echo ERROR: APK generation did not complete.
exit /b 1

別のBATであるgradlew.batを呼ぶ箇所にはcallを付け、Gradleが終わったら後続の変換へ戻るようにする。各段階でエラーを確認し、途中で失敗した場合に成功表示へ進ませない。

掲載版では、APKを一時ファイルへ取り出してから完成名へ置き換える形に整理した。ビルドや抽出が失敗した場合、以前の完成APKが残ることがあるので、最新版ができたかどうかはSUCCESSの表示で判断する。

そして、Universal APKの生成に成功した

成功時の処理は3段階。Debug App Bundleをビルドし、bundletoolでUniversal APKを生成し、最後にAPKセットの中から取り出す。

[1/3] Building the Debug App Bundle...
[2/3] Generating one Universal APK from the App Bundle...
[3/3] Extracting universal.apk...

SUCCESS: Universal APK created.

完成したファイルは、プロジェクト内の次の場所へ出力された。

apk_output\BookBlogAI-debug-universal.apk
出た。「SUCCESS」。
これで1個のAPKになった。
保存するのは、このファイル?
そう。今回、スマホへコピーして使うのはそのAPKだ。

ビルドログではデバッグ用キーストアで署名されており、今回の成果物は自分用のデバッグAPKである。ここで確認できたのはUniversal APKの生成成功まで。統合後の端末上での動作は、インストールして確認する段階になる。

スマホへインストールする

生成されたBookBlogAI-debug-universal.apkをスマホへコピーし、ファイルから開いてインストールする。端末がインストール元の許可を求めた場合は、使用しているファイルアプリなどに対して許可する。

USB接続で入れる場合は、USBデバッグを有効にし、PCからの接続を端末側で許可したうえで、プロジェクト直下から次を実行する。Android SDKを標準の場所へ入れた環境の例で、ユーザー名は環境変数に置き換えている。

"%LOCALAPPDATA%\Android\Sdk\platform-tools\adb.exe" install -r "apk_output\BookBlogAI-debug-universal.apk"

-rは既存アプリへの再インストールを指定する。更新として扱えるには、パッケージ名や署名などが既存アプリと整合している必要がある。今回の生成ログでは開発用のデバッグキーストアが使用された。更新時の署名の考え方はAndroid公式:アプリへの署名を参照してほしい。

これを1個入れれば、設定や過去履歴も別のスマホへ移る?
APKはアプリのプログラムだ。端末に保存した設定や履歴を持ち運ぶバックアップではない。

Gemmaのモデルファイルは別に用意する

今回1個にまとめるのは、アプリ本体とTensor用runtimeなどのビルド対象である。アプリから選んで読み込むGemmaの.litertlmモデルは、この構成では別途取り込むファイルとして扱っている。

新しい端末へインストールしただけで、以前の端末のモデル・設定・履歴まで揃うわけではない。対応する端末とモデルを用意し、アプリ内で必要な設定を行う。

同じアプリへの更新でデータが維持される場合と、新規インストールは分けて考える。自分用APKを残すときも、ソースプロジェクトと署名に使ったキーストアは別に保管しておきたい。

隣にある「.apks」は何なのか

出力先を見ると、BookBlogAI-debug.apksという似た名前のファイルもできていた。こちらはAPKセットを格納した入れ物で、今回使いたいAPKを取り出す途中に生成される。

APKは分かった。でも、末尾にsが付いたファイルもある。
.apksの方は入れ物だ。今回は、その中からuniversal.apkを取り出した。
両方スマホへ持っていく?
今回の方法でインストールするために必要なのは、取り出したBookBlogAI-debug-universal.apkの方だ。
ようやく、どれを残せばいいかはっきりした。

今後ソースを更新したときも、生成用BATを実行して最新版のAPKを作る。毎回Bundleの変換手順を手で打ち直さずに済むようになった。

build_universal_apk.bat

完成APKと、再ビルドに使うものを分けて残す

インストールするときに渡すのは1個のAPK。後で直したいときには、ソースや生成ツールも必要になる。今回の保存物は、次のように分けると分かりやすい。

保存するもの用途
BookBlogAI-debug-universal.apkスマホへのインストール
ソースプロジェクトと生成用BAT修正してAPKを作り直す
bundletoolのJARBundleからAPKを生成する
使用した署名用キーストア同じ署名で更新版を作る
対応するGemmaモデルアプリへ読み込んで文章生成に使う

.aab.apksは今回の生成工程の途中にできるものなので、APKをインストールするために一緒にコピーする必要はない。ソースとビルド環境が揃っていれば再生成できる。

本の感想を書きたい、から始まったアプリの完成

最初にやりたかったのは、本を読んだあと、写真と感想をブログに残すことだった。途中で端末内AIを使う方向へ進み、Tensor G5でGemmaを動かし、そこから投稿画面、書誌情報の確認、Blogger連携、履歴管理までつながった。

完成したアプリで行うことは、本の写真を選び、書名や感想、評価を入力して記事を作ること。裏側では、端末内の文章生成と通信処理を組み合わせ、Bloggerの下書きまで仕上げる。

なかなかいいアプリになった。
最初の「感想をブログへ残したい」から、ずいぶん作ったな。
作ってる途中のブログ記事を書くの、すっかり忘れてたけど。
アプリの方が先に完成したわけか。
その続きを第3回から書いて、これで第6回。ようやく記事も完成に追いついた。
その記事を作っている間に、また機能を増やすなよ。
……今のところは、これで完成。

AIが動くことを確認するテスト画面から、自分の読書記録を残すアプリへ。最後にインストール用のAPKも作成できた。ここで一度開発を区切り、実際の読書に使っていく。

完成したアプリのスクリーンショット

この記事について

この記事は実際の開発内容をもとに、説明の順序を整理し、登場人物の会話として再構成しています。構成・文章・会話の作成にはOpenAIのChatGPTを使用しました。掲載コマンドとBATは実際に成功した生成処理をもとに整理したサンプルで、個人の作業パスや認証情報は掲載していません。アプリ内の文章生成には、端末内で実行するGemmaを使用しています。

検索

年月ごとアーカイブ

フレンドブログ

Translate

自己紹介

自分の写真
映画が好き! 音楽好き! 演奏も好き! ミキシングが一番好き!

QooQ