方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【Flutter】トップ画面に日替わりのGIF画像とテキストを表示する。

この記事は約13分で読めます。

はじめに

方言を話す、おしゃべり猫型ロボット「ミーア」を開発中。

ミーア
おしゃべり猫型ロボット「ミーア」は、100以上の種類の豊かな表情で、悲しみや喜びを共有します。様々な性格(皮肉・おせっかい・ロマンチスト・天然)や方言(大阪弁・博多弁・鹿児島弁・広島弁)も話してくれます。ミーアとの暮らしで、毎日の生活をもっ...

現在のアプリトップ画面は下記。
2023年8月時点の、まだプロトタイプ状態だったミーアの静止画が表示されているのみ。

より、ユーザーに毎日アプリを開いて楽しんでもらえるように、下記実装を追加する。

  • ミーアの静止画をGIFアニメーションにする
  • GIFアニメーションに加えて、「本日のミーアの一言」を表示
  • GIFアニメーションと一言フレーズを日替わりで表示して、毎日の変化をユーザーに楽しんでもらう。

GIF画像とテキストの用意

まず、表示したいGIF画像とテキストのリストを準備する必要がある。

GIF画像に関しては、ミーアではさまざまな目の表情の画像を用意しており、下記の記事で、目の画像をアップロードすればそのまま動作検証とGIFのダウンロードできる関数を作成したので、こちらを利用。

こんな感じでgif画像をいくつか作成してみた。

gif画像をpubspec.yamlに記載

Flutterプロジェクトで画像やその他のアセットを使用する際には、それらをpubspec.yamlに記載してFlutterに知らせる必要がある。これにより、ビルドプロセス中にFlutterがアセットをアプリのアセットバンドルに含めることができる。

assets/images配下にGIF画像を配置して、それをpubspec.yamlに記載する。

pubspec.yamlにアセットセクションを追加し、画像のパスを記載。フォルダ内のすべての画像を一度に追加したい場合は、ディレクトリのパスを記述し、末尾に/を付けて、フォルダ内のすべてのファイルを含めるようにする。

Dart
flutter:
  assets:
    - assets/images/

もしくは、特定のファイルだけを明示的に追加する場合は、ファイル名を個別に記載する。

Dart
flutter:
  assets:
    - assets/images/specific_image.gif
    - assets/images/another_image.gif

pubspec.yamlに記載した後は、flutter pub getを実行して変更を有効にする。これで、Image.assetなどのウィジェットを使用してアセットを読み込むことができる。今回は、多くのgif画像を取り込むので、imagesディレクトリをassetsとして指定。

日替わりのメッセージとGIFを取得する関数を実装

画像とテキストを扱う関数を別ファイルに分け、それをトップ画面から呼び出すようにする。daily_message_service.dartという新しいファイルを作成し、その中に日替わりのメッセージとGIFを取得する関数を実装する。

daily_message_service.dartはアプリケーションのビジネスロジックを担当するサービスクラスになるので、servicesディレクトリに記載。

YAML
lib/
└── services/
    └── daily_message_service.dart

選択されたアイテムの保存と読み込み

  • 選択されたGIFとテキストのインデックスをローカルストレージ(例:SharedPreferences)に保存し、同じ日にアプリが再度開かれたときに同じアイテムを表示できるようにする。
  • DartのDateTimeクラスを使用して現在の日付を取得し、Randomクラスでその日付をシード値として使用。これにより、同じ日にアプリを開いた場合は常に同じアイテムが選ばれる。

日付の変更を検出

  • ユーザーがアプリを開いたときに現在の日付をチェックし、最後に保存された日付と異なる場合は新しいアイテムを選択して保存する。
Dart
// lib/services/daily_message_service.dart
import 'dart:math';
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';

class DailyMessageService {
  Future<Map<String, String>> getDailyMessageAndGif() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    var now = DateTime.now();
    var formatter = DateFormat('yyyyMMdd');
    String formattedDate = formatter.format(now);

    String lastPickedDate = prefs.getString('last_picked_date') ?? "";
    Map<String, String> result = {
      'gif': 'default.gif',
      'message': 'デフォルトメッセージ'
    };

    if (formattedDate != lastPickedDate) {
      // 日付が変わったら新しいメッセージとGIFを選ぶ
      var rng = Random(now.millisecondsSinceEpoch);
      List<String> gifs = [
        'mia1.gif',
        'mia2.gif'
      ]; // assets/imagesからの相対パス(つまりファイル名)を記述
      
      List<String> messages = [
        'ふぅ〜。思考停止',
        'リラックスタイム、最高だね',
        '「なんか面白い話して」って残酷すぎない?',
        '今日は、パンケーキ食べちゃおーっと',
        'この本読み始めたら止まらない、寝不足必至だな',
        'ご注文の料理を持ってきましたニャン',
        '健康診断の結果見るの、怖いー'
      ];

      result['gif'] = gifs[rng.nextInt(gifs.length)];
      result['message'] = messages[rng.nextInt(messages.length)];

      // 選んだメッセージとGIFを保存
      prefs.setString('last_picked_date', formattedDate);
      prefs.setString('daily_gif', result['gif']);
      prefs.setString('daily_message', result['message']);
      
      print('Stored GIF: ${result['gif']}');
      print('Stored Message: ${result['message']}');
    } else {
      // 同じ日なら保存されたメッセージとGIFを使用
      result['gif'] = prefs.getString('daily_gif') ?? 'default.gif';
      result['message'] = prefs.getString('daily_message') ?? 'デフォルトメッセージ';
    }
    
    return result;
  }
}

ホーム画面で日替わり表示関数を呼び出す

Dart
// lib/screens/home/home_tab.dart
import 'daily_message_service.dart';

class _HomeTabState extends ConsumerState<HomeTab> {
  // ... (その他の状態変数は省略) ...
  String _dailyGif = 'default.gif';
  String _dailyMessage = 'デフォルトメッセージ';

  @override
  void initState() {
    super.initState();
    _loadDailyMessage();
  }

  void _loadDailyMessage() async {
    DailyMessageService service = DailyMessageService();
    var dailyInfo = await service.getDailyMessageAndGif();
    setState(() {
      _dailyGif = dailyInfo['gif'] ?? 'default.gif';
      _dailyMessage = dailyInfo['message'] ?? 'デフォルトメッセージ';
    });
  }

  @override
  Widget build(BuildContext context) {
    // ... (ビルドメソッドのコードは省略) ...

    return Align(
      alignment: Alignment.center,
      child: Column(
        children: [
          InkWell(
            onTap: () async {
              final message = await grpcService.sayHello('Clocky');
              debugPrint('message: ${message.message}');
            },
            child: Image.asset('assets/images/$_dailyGif',
                width: 250, height: 250),
          ),
          Spacing.h8(),
          Text(
            _dailyMessage,
            style: TextStyle(
              fontSize: 16.0,
              fontWeight: FontWeight.bold,
            ),
          ),
          // ... (その他のウィジェットは省略) ...
        ],
      ),
    );
  }
}

動作確認

トップ画面を開くと、下記のように無事、デフォルトのGIF画像とテキストが表示された。

次に、日付を変えたときに、GIFアニメーションとフレーズが変わるかを検証。

今回は簡易的に、トップ画面に「1日進める」ボタンを設置して、そのボタンが押されたときに日付を変更し、それに応じてデータを更新するようにする。

simulatedDateProviderという名のStateProviderを用いて、シミュレーションされている現在の日付を管理する。ボタンが押されたときに、StateProviderの値を更新し、_loadDailyMessageメソッドを呼び出し、DailyMessageServiceを使用して新しい日付に基づいてデータを取得する。

DailyMessageService内でgetCurrentDateをオーバーライドし、プロバイダーから読み取った新しい日付を使用してサービスがデータを正しく取得できる。

Dart
// lib/screens/home/home_tab.dart

final simulatedDateProvider =
    StateProvider<DateTime>((ref) => DateTime.now()); //日付シミュレーション用
    
class _HomeTabState extends ConsumerState<HomeTab> {
  @override
  void initState() {
    super.initState();
    final user = ref.read(userProvider);
    volume = user?.volume ?? 50;
    _loadDailyMessage();
  }
  
  void _advanceDay() {
    // 現在のシミュレートされた日付を1日進める
    ref.read(simulatedDateProvider.notifier).update((state) => state.add(Duration(days: 1)));

    // 新しい日付でデータを再取得する
    _loadDailyMessage();
  }

  void _loadDailyMessage() {
    DailyMessageService service = DailyMessageService();
    var date = ref.read(simulatedDateProvider);
    service.getCurrentDate = () => date;  // 日付をServiceに渡す
    service.getDailyMessageAndGif().then((result) {
      // 結果に基づいてUIを更新する
      setState(() {
        // 画像やメッセージを更新
      });
    });
  }

  Widget build(BuildContext context) {
    final simulatedDate = ref.watch(simulatedDateProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: () {
                // ボタンが押された時の処理
                _advanceDay();
              },
              child: Text('1日進める')
            ),
            // その他のウィジェット...
          ],
        ),
      ),
    );
  }

  
}
Dart
flutter: Stored GIF: mia1.gif
flutter: Stored Message: 健康診断の結果見るの、怖いー
flutter: Stored GIF: mia1.gif
flutter: Stored Message: 健康診断の結果見るの、怖いー
flutter: Stored GIF: mia1.gif
flutter: Stored Message: 朝の体重計、いつもドキドキ
flutter: Stored GIF: mia2.gif
flutter: Stored Message: こわい夢見ちゃった〜

この画面で、音量調整のスライダーの下にある「1日進める」ボタンをクリックすると、local storageに変更後のGIFアニメーションとテキストが表示され、UIにも反映された。

この改修で、だいぶインタラクティブな、ホーム画面になったと思う。

コメント

タイトルとURLをコピーしました