Flutterアプリ「Kokokara」を支えるshared_preferences活用術

## はじめに

**Kokokara**は、日々の学びや気づきを手軽に得られるFlutterアプリです。  
このアプリでは、ユーザーが選択したカテゴリーに応じて、以下の2種類のメッセージを表示します。

- **今日の気づき**  
  自分の視点を広げるような気づきを提供するメッセージ  
- **一歩踏み出すヒント**  
  行動を促し、次のステップをサポートするアドバイス的なメッセージ  

また、気に入ったメッセージはお気に入りに登録・削除が可能で、お気に入り画面からいつでも見返すことができます。  
「**新しいメッセージを読む**」ボタンをタップすると、新たな「今日の気づき」や「一歩踏み出すヒント」が表示され、日々の学習やモチベーション維持に役立ちます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953880/2efabdcb-fcb2-4e43-b1b6-3f58c0070fd0.png)


**アプリは以下からダウンロードできます：**
[App StoreでKokokaraをダウンロード](https://apps.apple.com/jp/app/kokokara/id6740821050)
[Google PlayでKokokaraをダウンロード](https://play.google.com/store/apps/details?id=com.kokokara&hl=ja)

## Kokokaraの主な機能

1. **「今日の気づき」と「一歩踏み出すヒント」のメッセージ表示**  
   - 学びや気づきをサポートする2種類のメッセージを、ユーザーに合わせて画面に表示します。

2. **カテゴリーの選択に応じたメッセージの切り替え**  
   - ユーザーの興味・関心に合わせて、該当カテゴリーのメッセージを取得・表示します。

3. **お気に入り登録・削除**  
   - 気に入ったメッセージをお気に入りとして保存し、あとで見返すことができます。  
   - 不要になった場合はお気に入り一覧から削除して管理できます。

4. **お気に入り画面での確認**  
   - 登録したお気に入りメッセージは、お気に入り画面で確認ができます。


## shared_preferencesの活用

Kokokaraでは、以下のような情報をローカル端末に保存するため、[shared_preferences](https://pub.dev/packages/shared_preferences)を用いています。

- **message_history**: 取得・表示したメッセージの履歴  
- **selectedCategories**: ユーザーが選択したカテゴリー情報  
- **isFirstLaunch**: アプリの初回起動かどうかのフラグ  
- **favorite_messages**: お気に入りに登録したメッセージ一覧  

### shared_preferencesとは？

[shared_preferences](https://pub.dev/packages/shared_preferences)は、Flutterアプリで軽量データをローカルに保存する際に便利なパッケージです。  
キーとバリューのペアでシンプルにデータを保存・取得でき、データベースを構築せずに設定情報やリストデータを扱いたいときに重宝します。

## Kokokaraでの実装例

Kokokaraでは、**JSON文字列**としてデータを保存・読み込みすることで、配列やオブジェクトなどの複雑な構造も容易に扱っています。  
以下は、`PreferenceService`クラスの例です。

```dart
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

class PreferenceService {
  static const String keyMessageHistory = "message_history";
  static const String keySelectedCategories = "selectedCategories";
  static const String keyIsFirstLaunch = "isFirstLaunch";
  static const String keyFavoriteMessages = "favorite_messages";

  // -------------------------------------
  // メッセージ履歴の読み込み
  // -------------------------------------
  Future<List<String>> loadMessageHistory() async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = prefs.getString(keyMessageHistory);
    if (jsonString == null) {
      return [];
    }
    final List<dynamic> decoded = json.decode(jsonString);
    return decoded.cast<String>();
  }

  // -------------------------------------
  // メッセージ履歴の保存
  // -------------------------------------
  Future<void> saveMessageHistory(List<String> messages) async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = json.encode(messages);
    await prefs.setString(keyMessageHistory, jsonString);
  }

  // -------------------------------------
  // 選択されたカテゴリーの読み込み
  // -------------------------------------
  Future<List<String>> loadSelectedCategories() async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = prefs.getString(keySelectedCategories);
    if (jsonString == null) {
      return [];
    }
    final List<dynamic> decoded = json.decode(jsonString);
    return decoded.cast<String>();
  }

  // -------------------------------------
  // 選択されたカテゴリーの保存
  // -------------------------------------
  Future<void> saveSelectedCategories(List<String> categories) async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = json.encode(categories);
    await prefs.setString(keySelectedCategories, jsonString);
  }

  // -------------------------------------
  // 初回起動フラグの読み込み
  // -------------------------------------
  Future<bool> loadIsFirstLaunch() async {
    final prefs = await SharedPreferences.getInstance();
    // 値がない場合はtrueを返す（初回起動として扱う）
    return prefs.getBool(keyIsFirstLaunch) ?? true;
  }

  // -------------------------------------
  // 初回起動フラグの保存
  // -------------------------------------
  Future<void> saveIsFirstLaunch(bool isFirst) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(keyIsFirstLaunch, isFirst);
  }

  // -------------------------------------
  // お気に入りメッセージ一覧の読み込み
  // -------------------------------------
  Future<List<String>> loadFavoriteMessages() async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = prefs.getString(keyFavoriteMessages);
    if (jsonString == null) {
      return [];
    }
    final List<dynamic> decoded = json.decode(jsonString);
    return decoded.cast<String>();
  }

  // -------------------------------------
  // お気に入りメッセージ一覧の保存
  // -------------------------------------
  Future<void> saveFavoriteMessages(List<String> favoriteMessages) async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = json.encode(favoriteMessages);
    await prefs.setString(keyFavoriteMessages, jsonString);
  }
}
```
- `List<String>`のように、配列を保存したい場合は、JSONでエンコードしてからSharedPreferencesに保存

- 取り出すときは、JSON文字列をデコードして`List<dynamic>`を取得し、`cast<String>()`などで型変換します

## 実装のポイント

### カテゴリ切り替え

- ユーザーが選択したカテゴリ（例：`selectedCategories`）を `SharedPreferences` に保存  
- アプリ起動時や画面表示時に読み込み、該当するメッセージだけを表示

### メッセージ表示

- ボタンタップ時に、新たな「今日の気づき」「一歩踏み出すヒント」を取得  
- 取得したメッセージを表示し、必要に応じて `message_history` などに保存

### お気に入り機能

- お気に入り登録するメッセージIDやテキストを `favorite_messages` に保存  
- 解除時は、該当のIDやテキストをリストから削除して再度保存  
- お気に入り画面で、保存しているリストを読み込み・表示

### 初回起動の判定

- `isFirstLaunch` フラグを利用し、チュートリアルやガイドを表示するかどうかを制御

## まとめ

### Kokokaraの特徴

- 「今日の気づき」「一歩踏み出すヒント」の2種類のメッセージで、日々の気づきをサポート  
- カテゴリー切り替えにより、興味のある分野のメッセージを表示  
- 気になるメッセージはお気に入り登録で管理し、いつでも見返し可能  
- 「新しいメッセージを読む」ボタンで、常に新しい学びを得られる

### shared_preferencesを用いるメリット

- 端末のローカルにシンプルかつ軽量にデータを保存  
- データベース構築の必要がなく、キー・バリュー形式で扱いやすい  
- JSON形式で複雑なデータ（リストやオブジェクト）も自由に操作できる

Flutterアプリで軽量かつシンプルなローカル保存をしたいときは、ぜひ`shared_preferences`を試してみてください。  
Kokokaraのように**ユーザーごとに異なる学びのコンテンツを表示するアプリ**でも、キー・バリュー形式のローカル保存が大いに役立ちます。

---

もし興味を持っていただけた方は、ぜひ「Kokokara」を手に取ってみてください。  
ご質問やフィードバックもお気軽にお寄せください！



## 参考リンク

- [shared_preferences (pub.dev)](https://pub.dev/packages/shared_preferences)