Wails 実践講座 #10 仕上げと完成度 — 設定・ダークモード・データバックアップ

読了 5分

シリーズ最後の記事です。#9 まで機能は揃いました。しかし機能が揃ったアプリと、毎日また開きたいアプリの間には仕上げの差があります。この記事はその仕上げ、つまり設定・ダークモード・ウィンドウ状態・データの安全を埋め、10 編全体を貫いた原則を振り返ってシリーズを閉じます。

このシリーズは全 10 編、2 部構成です。

設定: 何をどこに保存するか #

アプリが大きくなると、ユーザーが変えたい値が出てきます。フォントサイズ、テーマ、自動保存の遅延、ショートカットの組み合わせなどです。この設定はノートのデータと性格が異なります。検索の必要も関係もない単純なキー-バリューなので、#2 の SQLite に入れるより JSON ファイル一つが合います。場所は 入門 #5 で扱った os.UserConfigDir の下、ノート DB の隣です。

settings.go — 設定構造体と既定値
type Settings struct {
	Theme         string `json:"theme"`         // "system" | "light" | "dark"
	FontSize      int    `json:"fontSize"`
	AutosaveDelay int    `json:"autosaveDelay"` // ms, #7 のデバウンスと連結
}

func defaultSettings() Settings {
	return Settings{Theme: "system", FontSize: 15, AutosaveDelay: 600}
}

設定をサービスとして公開する方式はノートと同じです。GetSettingsSaveSettings をバインディングに置けば、フロントエンドが設定画面で読み書きします。重要なのは既定値をコードに置くことです。設定ファイルがないか(初回起動)、一部のキーが欠けていても既定値で埋め、アプリが常に完全な設定で始まるようにします。

ダークモード: システムに追従する #

テーマは設定の代表的な項目です。三つの値(system・light・dark)を置きますが、既定は system、つまり OS の設定に追従するのが実践の既定値です。ユーザーが OS をダークに変えるとアプリも追って暗くなるのが自然です。フロントエンドでは CSS の prefers-color-scheme でシステムの値を受け、ユーザーが明示的に light/dark を選べばその選択が優先します。

theme.js — 設定とシステムを合わせる
function applyTheme(setting) {
  let theme = setting; // "light" | "dark" | "system"
  if (setting === "system") {
    theme = window.matchMedia("(prefers-color-scheme: dark)").matches
      ? "dark" : "light";
  }
  document.documentElement.dataset.theme = theme; // CSS がこの値で色を変える
}

色は CSS 変数で一箇所にまとめ、data-theme で切り替えると、画面のあちこちに色を撒かずにテーマを管理できます。

ウィンドウ状態の記憶 #

小さくても体感の大きい仕上げがウィンドウ状態です。ユーザーがウィンドウのサイズを調整して位置を移したのに、次に開くとき原点に戻っていると、毎回合わせ直す必要があります。アプリを閉じるとき(または #4 の非表示フックで)ウィンドウのサイズ・位置を設定ファイルに保存し、起動時にその値で復元します。Wails ランタイムのウィンドウのサイズ・位置の照会・設定の関数で読み書きします。マルチモニターで保存した位置が画面の外になる場合だけ防御すれば済みます。

データの安全: バックアップ・エクスポート・インポート #

ノートアプリで最も失ってはいけないのはユーザーのノートです。仕上げの核心はデータの安全で、三つを備えます。

  • バックアップ: アプリが定期的に DB ファイルをコピーしておけば、破損や誤削除から戻せます。SQLite はファイル一つなので、バックアップはそのままファイルのコピーです。
  • エクスポート: ユーザーが自分のノートをアプリの外に取り出せる必要があります。全ノートをマークダウンのファイル群か JSON 一つにエクスポートすれば、アプリを離れてもデータはユーザーのものとして残ります。これは信頼の問題でもあります。
  • インポート: エクスポートしたものを再び読み込んだり、別の端末から移してこられる必要があります。入門 #4 のファイルダイアログでファイルを選び、サービスがパースしてリポジトリに入れます。
export.go — 全ノートを JSON でエクスポート
func (s *NoteService) ExportJSON() ([]byte, error) {
	notes, err := s.repo.List()
	if err != nil {
		return nil, err
	}
	return json.MarshalIndent(notes, "", "  ") // 人が読める形で
}

エクスポート・インポートは #2 のリポジトリの上に自然に載ります。層を分けておいたおかげで、ノートを取り出して入れる新しい機能が、サービスにメソッドをいくつか加えるだけで済みます。

10 編の原則の振り返り #

10 編を貫いた決定を 1 行ずつ振り返ります。

  • #1 層の分離: バインディング・サービス・リポジトリを分け、依存を一方向に。この一つが #8 のテストと #10 のエクスポートを簡単にしました。
  • #2 純粋な Go の SQLite: CGO を避けた選択が #6 のクロスコンパイルと #8 の CI テストで見返りとして返ってきました。
  • #3 真実は Go に: データをイベントで流す設計が #4 のバックグラウンドキャプチャと #7 の一覧更新を 1 行で解決しました。
  • #4 バックグラウンド常駐#5 署名・公証#6 CI 自動化でアプリを実際に配信可能にし、
  • #7 エディタ#8 テスト#9 自動更新#10 仕上げでまた開きたい完成度を埋めました。

貫く教訓は一つです。序盤の構造の決定(層の分離、純粋な Go、真実は Go に)が終盤のすべての機能を安くするということです。実践は派手な機能ではなく、これらの決定が互いを支え合うところから生まれます。

締めくくり #

入門シリーズが Wails の文法と概念を扱ったとすれば、この実践シリーズは一つのアプリを設計から配信、そしてユーザーがまた訪れる完成度まで持っていきました。層を分け、CGO を避け、検索を載せ、バックグラウンドに常駐させ、署名し、自動化し、テストし、更新を付け、仕上げました。ここまで来たなら、一つのアイデアを実際のユーザーが毎日使うデスクトップアプリにする全行程を手にしたことになります。以上で Wails 実践講座を終えます。

まとめ #

  • 設定は検索・関係のない単純なキー-バリューなので、SQLite ではなく JSON ファイル一つに置きます。既定値をコードに置いて、ファイルがなくても完全に始まります。
  • ダークモードの既定はシステムに追従することです。色は CSS 変数にまとめ、data-theme で切り替えます。
  • ウィンドウのサイズ・位置を保存・復元し、毎回合わせ直さないようにします。画面の外の位置だけ防御します。
  • データの安全はバックアップ・エクスポート・インポートで備えます。エクスポートは信頼の問題で、層の分離のおかげでリポジトリの上に簡単に載ります。
  • 10 編の教訓は一つです。序盤の構造の決定が終盤のすべての機能を安くします。以上でシリーズを終えます。
X