電卓メモ — メモ感覚で数字を自動計算するAndroidアプリを個人開発した記録

買い物中に「牛乳220、パン180…」とメモしながら合計を把握したい。トランプやボードゲームのスコアを対局中にサッと記録したい。そんな「電卓を開いて数字を打ち直す」「メモ帳に計算結果を書き写す」手間をなくすため、メモ感覚で自由に文字と数字を書き込みながら、数字の部分だけを自動で抽出・計算してくれるAndroidアプリ「電卓メモ」を個人開発しました。本記事では、その設計・実装・公開までの過程を技術的な観点からまとめます。

1. 電卓メモ とは

1-1. 開発の背景・課題

普通の電卓アプリは、数字を打ち直す手間がかかるうえ「何を計算したか」の記録が残りません。かといって普通のメモ帳では、書いた数字を自分で暗算・集計する必要があります。この「電卓」と「メモ帳」の間にある溝を埋めるため、文章の中に書いた数字を自動で抽出・計算するメモアプリを作ることにしました。

1-2. 既存アプリ・ツールとの比較

ツール自由な文章入力数字だけ自動計算小計・合計の自動集計ログイン
標準の電卓アプリ◯(数式のみ)不要
汎用メモ帳アプリ不要
表計算アプリ△(セル単位)必要な場合あり
電卓メモ不要

2. 技術スタック

UI・状態管理・永続化を明確に分離したMVVM構成を採用しています。

技術スタック構成 UI層(Jetpack Compose) NoteEditScreen / NoteTabBar / NoteLineList / NumpadPanel / SettingsPanel Material3・DentakuColors(独自デザイントークン)・12色テーマ・SplashScreen API ViewModel層(MVVM) NoteEditViewModel / SettingsViewModel / BackupViewModel StateFlow + Hilt DI(@HiltViewModel) Repository層 NoteRepository / SettingsRepository / AdFreeRepository / BackupService Coroutines Flow・Room Transaction・Google Play Billing連携 データ層 Room(NoteEntity / LineEntity) / DataStore(AppSettings) 端末内のみに保存、外部サーバー・アカウントなし
分類技術役割
言語Kotlinアプリ全体
UIJetpack Compose + Material3宣言的UI構築
アーキテクチャMVVMUIとロジックの分離
DIHilt依存性注入
ローカルDBRoomノート・行データの永続化
設定保存Jetpack DataStoreテーマ・桁数などのアプリ設定
非同期処理Kotlin Coroutines / FlowDB監視・状態通知
広告Google Mobile Ads SDK(AdMob)バナー広告表示
課金Google Play Billing Library 8.0広告非表示の買い切り課金

3. アーキテクチャ・データフロー

3-1. アーキテクチャ解説

「自由に書いた文章から数字だけを抽出・計算する」というコア機能は、UIから独立したdomain/usecase層(NumberExtraction・FormulaEvaluation・SubtotalCalculation)に切り出しており、単体テストだけで正しさを検証できるようにしています。ユーザーがテキストを入力すると、ViewModelがRoomへ書き込み、Room のFlow経由で画面に自動的に反映される一方向のデータフローです。

データフロー(自由記述 → 自動計算 → 表示) NoteLineRow ユーザー入力 FormulaEvaluation 数式判定・四則演算 SubtotalCalculation NoteEditViewModel runInTransaction 末尾空行自動補充 Room DB LineEntity observeLines() Flow Flowで再購読 → 画面に自動反映(計算結果・小計・合計バッジ)

3-2. 広告・課金のシーケンス

ログイン機能を持たないアプリのため、外部通信はAdMobの広告配信とGoogle Play Billingの購入処理に限られます。特に購入処理は、Play Consoleが発行するライセンス公開鍵を使い、購入レスポンスの署名をアプリ側でも検証することで、改ざんされた・偽装された購入情報を弾く仕組みにしています。

広告表示・購入処理のシーケンス アプリ Google AdMob Google Play Billing 広告リクエスト バナー広告を返却 launchBillingFlow(remove_ads) Purchase(署名付きJSON)を返却 verifyPurchaseSignature() ライセンス公開鍵でSHA1withRSA検証 acknowledgePurchase(検証OKの場合のみ) 検証NGの購入は無視しログ出力(改ざん・不正購入への対策)

3-3. DBスキーマ

Roomで管理するテーブルはnoteslinesの2つのみとシンプルです。小計行が参照する元行のIDを保持するsubtotalRefIdsはv1にはなく、小計の自動再計算を実現するためにマイグレーションで追加しました。

DBスキーマ(Room, version 2) notes id : Long (PK) title : String order : Int createdAt : Long updatedAt : Long lines id : Long (PK) noteId : Long (FK→notes, CASCADE) order : Int rawText : String lineType : NORMAL/FORMULA/SUBTOTAL formulaOff : Boolean subtotalRefIds : String(v2で追加) 1:N アプリ設定(テーマ・桁数・広告非表示状態など)はDataStoreで別管理(Roomの対象外)

4. 画面構成・機能一覧

Navigation Composeは使わず、単一画面の中でタブによってノートを切り替える構成にしています。画面遷移がないぶん、状態管理がシンプルになります。

画面構成(単一画面 + タブ切替) NoteEditScreen MainActivity単一画面 NoteTabBar ノート切替・追加・並替 NoteLineList 行一覧・範囲選択・小計 NumpadPanel 専用数字キーボード SettingsPanel テーマ・バックアップ・課金 SpotlightTutorialOverlay(初回起動時・設定から再生可)
画面/機能説明
自由記述+自動計算文章内の数字を自動抽出。四則演算・括弧・%にも対応
小計機能範囲を長押し選択して小計行を挿入。元行を編集すると自動再計算
「計算しない」設定日付・電話番号など計算対象から外したい行をワンタップで除外
専用数字キーボード四則演算記号・括弧・%に特化した入力パネル
複数ノート管理用途ごとにタブを分けて管理・並び替え可能
テーマ・見た目のカスタマイズダーク/ライト×6色の計12テーマ、文字サイズ、装飾エフェクト
バックアップ・復元JSONファイルとして端末内に書き出し・読み込み
広告非表示(買い切り)Google Play Billingによる任意のアプリ内課金

5. 使い方ガイド

5-1. 初回セットアップ

  1. Google Playからインストールし、アプリを起動する(アカウント登録は不要)
  2. 初回起動時に表示されるチュートリアルで基本操作を確認する
  3. サンプルノート「買い物」で自由記述・小計・専用キーボードの動きを試す

5-2. 主要機能の使い方

  1. 「牛乳」と書いて改行し「220」と入力すると、右側に金額が自動表示される
  2. 同様に「パン」「180」、「みかん」「100×3」と書き足す
  3. まとめて範囲選択し「合計」ボタンを押すと小計行が挿入される
  4. 画面左下には常にノート全体の合計が表示され続ける
  5. 右下のボタンで標準キーボードと専用数字キーボードを切り替えられる

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が一瞬だけ誤表示される問題

StateFlowstateIn()で公開する際、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で計測したところ、ジャンキーフレームが発生していました。原因は、最終行の座標(スクロールごとに変化する)を直接読んで毎フレーム再コンポーズしていたことでした。座標の書き込み自体(選択ドラッグの当たり判定に必須)は残しつつ、親コンポーネントへの通知だけをMutableStateFlowdebounce(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アプリ実装例として、少しでも参考になれば幸いです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA