買い物中に「牛乳220、パン180…」とメモしながら合計を把握したい。トランプやボードゲームのスコアを対局中にサッと記録したい。そんな「電卓を開いて数字を打ち直す」「メモ帳に計算結果を書き写す」手間をなくすため、メモ感覚で自由に文字と数字を書き込みながら、数字の部分だけを自動で抽出・計算してくれるAndroidアプリ「電卓メモ」を個人開発しました。本記事では、その設計・実装・公開までの過程を技術的な観点からまとめます。
1. 電卓メモ とは
1-1. 開発の背景・課題
普通の電卓アプリは、数字を打ち直す手間がかかるうえ「何を計算したか」の記録が残りません。かといって普通のメモ帳では、書いた数字を自分で暗算・集計する必要があります。この「電卓」と「メモ帳」の間にある溝を埋めるため、文章の中に書いた数字を自動で抽出・計算するメモアプリを作ることにしました。
1-2. 既存アプリ・ツールとの比較
| ツール | 自由な文章入力 | 数字だけ自動計算 | 小計・合計の自動集計 | ログイン |
|---|---|---|---|---|
| 標準の電卓アプリ | ✕ | ◯(数式のみ) | ✕ | 不要 |
| 汎用メモ帳アプリ | ◯ | ✕ | ✕ | 不要 |
| 表計算アプリ | △(セル単位) | ◯ | ◯ | 必要な場合あり |
| 電卓メモ | ◯ | ◯ | ◯ | 不要 |
2. 技術スタック
UI・状態管理・永続化を明確に分離したMVVM構成を採用しています。
| 分類 | 技術 | 役割 |
|---|---|---|
| 言語 | Kotlin | アプリ全体 |
| UI | Jetpack Compose + Material3 | 宣言的UI構築 |
| アーキテクチャ | MVVM | UIとロジックの分離 |
| DI | Hilt | 依存性注入 |
| ローカルDB | Room | ノート・行データの永続化 |
| 設定保存 | Jetpack DataStore | テーマ・桁数などのアプリ設定 |
| 非同期処理 | Kotlin Coroutines / Flow | DB監視・状態通知 |
| 広告 | Google Mobile Ads SDK(AdMob) | バナー広告表示 |
| 課金 | Google Play Billing Library 8.0 | 広告非表示の買い切り課金 |
3. アーキテクチャ・データフロー
3-1. アーキテクチャ解説
「自由に書いた文章から数字だけを抽出・計算する」というコア機能は、UIから独立したdomain/usecase層(NumberExtraction・FormulaEvaluation・SubtotalCalculation)に切り出しており、単体テストだけで正しさを検証できるようにしています。ユーザーがテキストを入力すると、ViewModelがRoomへ書き込み、Room のFlow経由で画面に自動的に反映される一方向のデータフローです。
3-2. 広告・課金のシーケンス
ログイン機能を持たないアプリのため、外部通信はAdMobの広告配信とGoogle Play Billingの購入処理に限られます。特に購入処理は、Play Consoleが発行するライセンス公開鍵を使い、購入レスポンスの署名をアプリ側でも検証することで、改ざんされた・偽装された購入情報を弾く仕組みにしています。
3-3. DBスキーマ
Roomで管理するテーブルはnotesとlinesの2つのみとシンプルです。小計行が参照する元行のIDを保持するsubtotalRefIdsはv1にはなく、小計の自動再計算を実現するためにマイグレーションで追加しました。
4. 画面構成・機能一覧
Navigation Composeは使わず、単一画面の中でタブによってノートを切り替える構成にしています。画面遷移がないぶん、状態管理がシンプルになります。
| 画面/機能 | 説明 |
|---|---|
| 自由記述+自動計算 | 文章内の数字を自動抽出。四則演算・括弧・%にも対応 |
| 小計機能 | 範囲を長押し選択して小計行を挿入。元行を編集すると自動再計算 |
| 「計算しない」設定 | 日付・電話番号など計算対象から外したい行をワンタップで除外 |
| 専用数字キーボード | 四則演算記号・括弧・%に特化した入力パネル |
| 複数ノート管理 | 用途ごとにタブを分けて管理・並び替え可能 |
| テーマ・見た目のカスタマイズ | ダーク/ライト×6色の計12テーマ、文字サイズ、装飾エフェクト |
| バックアップ・復元 | JSONファイルとして端末内に書き出し・読み込み |
| 広告非表示(買い切り) | Google Play Billingによる任意のアプリ内課金 |
5. 使い方ガイド
5-1. 初回セットアップ
- Google Playからインストールし、アプリを起動する(アカウント登録は不要)
- 初回起動時に表示されるチュートリアルで基本操作を確認する
- サンプルノート「買い物」で自由記述・小計・専用キーボードの動きを試す
5-2. 主要機能の使い方
- 「牛乳」と書いて改行し「220」と入力すると、右側に金額が自動表示される
- 同様に「パン」「180」、「みかん」「100×3」と書き足す
- まとめて範囲選択し「合計」ボタンを押すと小計行が挿入される
- 画面左下には常にノート全体の合計が表示され続ける
- 右下のボタンで標準キーボードと専用数字キーボードを切り替えられる
6. 主な実装上のポイント
6-1. シード投入時のレースコンディション
初回起動時のサンプルデータ投入で、行と行の間に空行が混入するバグが発生しました。原因は、複数行を逐次INSERTする間にRoomのobserveLines() Flowが中間状態のまま再発火し、末尾空行の自動補充ロジックが未完成の状態に反応してしまうことでした。
suspend fun <T> runInTransaction(block: suspend () -> T): T =
database.withTransaction { block() }
// ensureSeedData() 内
database.withTransaction { insertSeedData() }
複数回に分けてRoomへ書き込む処理(シード投入・行の分割・行の結合)をすべてトランザクションで包み、操作完了までFlowへ中間状態が漏れないようにして解消しました。
6-2. 起動時にUIが一瞬だけ誤表示される問題
StateFlowをstateIn()で公開する際、DataStore/Roomの実際の読み込みが終わるまでの一瞬、initialValue(既定値)が画面に表示されてしまう問題が複数箇所で見つかりました。「設定値」と「読み込み完了フラグ」を別々のStateFlowとして公開していたため、Compose側の2つの独立した購読がアトミックに更新されず、一瞬だけちぐはぐな状態が見えてしまうケースもありました。
data class SettingsState(val settings: AppSettings = AppSettings(), val isLoaded: Boolean = false)
val settingsState: StateFlow<SettingsState> = repository.settings
.map { SettingsState(settings = it, isLoaded = true) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SettingsState())
「値」と「読み込み完了フラグ」を1つのStateFlowにまとめることで、常に両方が同時に更新されるようにし、この種のちらつきを構造的になくしました。
6-3. スクロールのジャンク
dumpsys gfxinfoで計測したところ、ジャンキーフレームが発生していました。原因は、最終行の座標(スクロールごとに変化する)を直接読んで毎フレーム再コンポーズしていたことでした。座標の書き込み自体(選択ドラッグの当たり判定に必須)は残しつつ、親コンポーネントへの通知だけをMutableStateFlowとdebounce(50)で切り離すことで解消しました。
6-4. 購入の署名検証
広告非表示の買い切り課金は、Play Consoleが発行するライセンス公開鍵を使い、Play Billingから返る購入レスポンスの署名をアプリ側でも検証しています。minSdkがAPI 26のためandroid.util.Base64ではなくjava.util.Base64を使うことで、JVM単体テストでも実際のRSA鍵ペアを生成して検証ロジックをテストできるようにしました。
internal fun verifyPurchaseSignature(
signedData: String,
signature: String,
publicKeyBase64: String = AdFreeRepository.LICENSE_KEY,
): Boolean = try {
val keyBytes = Base64.getDecoder().decode(publicKeyBase64)
val publicKey = KeyFactory.getInstance("RSA").generatePublic(X509EncodedKeySpec(keyBytes))
val sig = Signature.getInstance("SHA1withRSA")
sig.initVerify(publicKey)
sig.update(signedData.toByteArray(Charsets.UTF_8))
sig.verify(Base64.getDecoder().decode(signature))
} catch (_: Exception) {
false
}
7. 使用技術の解説
Jetpack Compose:Androidの宣言的UIツールキットです。画面の見た目を「状態(State)から導かれる関数」として書け、XMLレイアウトを使わずにUIを構築できます。
Room:AndroidのSQLiteラッパーで、テーブル定義をKotlinのdata classとアノテーションで書けます。今回はマイグレーションをfallbackToDestructiveMigration()に頼らず明示的なMigrationクラスとして実装し、既存ユーザーのデータを壊さずにスキーマ変更できるようにしています。
Hilt:Googleが提供する依存性注入(DI)フレームワークです。ViewModelやRepositoryの生成をフレームワーク側に任せることで、テスト時にモックへ差し替えやすい設計になります。
Google Play Billing Library:アプリ内課金を扱うための公式ライブラリです。2026年8月時点で、新規アプリ・アプリ更新はバージョン8以降の使用がGoogle Playの必須要件になっています。
8. 開発プロセス
| フェーズ | 内容 |
|---|---|
| 基本機能実装 | 自由記述・自動計算・数式評価・小計・専用数字キーボード・バックアップ機能を実装 |
| UI/UX改修 | 選択検出の一本化、テーマ選択UIの再設計、装飾エフェクト・文字サイズ設定を追加 |
| 公開準備・素材作成 | アプリアイコン・ストア掲載スクリーンショット・フィーチャーグラフィックを作成 |
| 広告・課金実装 | AdMobバナー広告、Google Play Billingによる広告非表示の買い切り課金を実装 |
| 品質改善 | レースコンディション修正、7観点でのチームレビュー、初回チュートリアル追加 |
| 性能・体感改善 | 起動速度改善、SplashScreen導入、スクロールジャンク改善、起動時ちらつき修正 |
| Play Console対応 | ストア掲載情報・データセーフティ等の申告、商品登録、購入の署名検証実装 |
| SDK追従・リリース | targetSdk 36・Billing Library 8.0への更新、正式リリース |
9. 今後の改善候補
- 数字キーボードFAB・設定ボタン開閉時のアニメーション・トランジションの作り込み
- ノート削除時の確認UIなど細部の挙動の見直し
- ドラッグ選択・タブのドラッグ&ドロップの操作感のさらなる磨き込み
- 複数端末間でのデータ同期(現状は手動のファイルエクスポート・インポートのみ)
10. 動作要件
- Android 8.0(API 26)以降
- Googleアカウント等のログイン:不要
- 通信環境:メモの作成・計算・保存はすべて端末内で完結し、オフラインでも利用可能(広告表示時を除く)
- データ保存先:すべて端末内のみ。外部サーバー・クラウドへの送信なし
シンプルながら、日々のちょっとした計算・集計をぐっと楽にするメモ帳アプリを目指して開発しました。個人開発のAndroidアプリ実装例として、少しでも参考になれば幸いです。
サラリーマンの相場道 
