【Flutter】介護アプリを作成したけど失敗した話

↓Chromecast with Google TV向けに作成した お薬飲み忘れ防止アプリ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167934/92528a64-4d47-b97f-d191-ed21b42f058e.png)

# 高齢者向け薬管理アプリの挫折と学び
先日、私は母親のために、薬の飲み忘れを防ぐためのアプリを作成しました。そのアプリは、母親がTVを通じて直感的に薬の服用時間を知ることができるように設計されていました。それはChromecast with Google TVに対応しており、デバッグビルドされ、私自身が配信しました。

## しかし、問題が発生しました。

母親が「TV画面の画像」と「手元の薬箱」の違いを認識する事が難しいという問題が発生しました。母親にとって、それらの薬箱の違いを認識することが困難であることが明らかとなりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167934/27256010-ad71-517e-6c58-2b1c83ed4e7a.png)

そして、私は失敗を認めました。しかし、その失敗が無駄ではないことを確信しています。

## 失敗から学ぶこと

失敗は、私たちが何か新しいことを試すたびに必然的についてくるものです。私たちは失敗から学び、それを次の成功へとつなげることができます。そして、私たちは共に頑張りましょう。私が作成したコードは、皆さんが自由にお使いいただけます。

この記事では、以下の項目について詳しく説明します：

- 高齢者のTV画面と薬箱の違いの認識の難しさ
- 少子高齢化に向けてエンジニアができること
- Chromecastの利点

## 高齢者のTV画面と薬箱の違いの認識の難しさ

私たちが自然に感じ取ることができる、色や形状、大きさの違いは、年齢を重ねるとともに認識が困難になります。それは特にテクノロジーと結びついた場面で顕著です。私の母親の場合、TV画面上で表示される薬箱のイメージの違いを認識することが困難でした。

テクノロジーとの接触が少ない高齢者にとって、デジタル画面上のビジュアル変化は、リアルな物体との関連性を理解するのが難しいことがあります。特に、デジタル画面上で色や形状が微妙に異なるものを識別することは困難な場合があります。それは、物理的な物体を手に取って見るという経験からくる直感的な認識が働かないからです。

また、テレビ画面自体が映像や情報を伝えるための媒体であり、それが持つ情報量の多さも高齢者にとっては混乱を招く要素となる可能性があります。TV画面上の薬箱のイメージが持つ情報は、リアルな薬箱の物理的な特性とは異なります。それゆえに、リアルな薬箱と画面上の薬箱のイメージとを一致させるのが難しくなるのです。

このような課題を理解することは、高齢者向けのテクノロジーを設計する際の重要な視点となります。

## 少子高齢化に向けてエンジニアができること

我々エンジニアは、社会問題を解決するための多くのツールと能力を持っています。特に少子高齢化社会という、現在の日本が直面している問題に対しては、テクノロジーとエンジニアリングが大きな役割を果たすことができます。

**アクセシビリティの改善**: テクノロジーを通じて高齢者が日常生活を自立して過ごせるように、アクセシビリティを向上することが必要です。例えば、画面の文字を大きくしたり、音声認識や読み上げ機能を追加したりといった簡単な改良が、高齢者の生活の質を大幅に向上させることができます。

**リモートケア技術**: 高齢者が自宅で安全に過ごせるように、リモートケア技術の開発が求められています。センサーやIoTデバイスを用いて生活状況をモニタリングし、必要に応じて適切なケアやサポートを提供するシステムは、介護負担を軽減し、高齢者の生活の質を向上させることができます。

**教育と啓蒙**: エンジニアとして、高齢者がテクノロジーを理解し、使用することを支援する教育的な役割を果たすことも重要です。また、自分たちが開発する製品やサービスが、高齢者にとって使いやすく、理解しやすいものであることを確認することも不可欠です。

今回私が経験したような失敗も、学びと向上の一部です。一度の失敗から、高齢者がテクノロジーをどのように認識し、それにどのように反応するかを理解しました。そして、これからはそれを念頭に置いて新たなアプリやシステムを設計し、開発するつもりです。

少子高齢化は、私たちが直面する社会の現実です。しかし、この問題に対処するためには、エンジニアとしての技術と創造力を最大限に活用することが必要です。これからも引き続き、高齢者の生活の質を向上させ、社会全体の幸福を追求するためのテクノロジー開発に尽力していきたいと思います。

## なぜChromecastにしたか
日頃から母はスマートフォンの操作が得意ではないようでしたし、小さい画面が見ずらい事もありChromecast＋古いPCのモニターにしました

## Chromecastの利点

Chromecastは、スマートフォンやタブレット、パソコンからテレビ画面へと簡単に映像や音声をストリーミングすることが可能なデバイスです。それだけでなく、Chromecastを使用した開発には以下のような特長があります。

**広範で直感的なユーザーインターフェース**: Chromecastを使用すれば、大画面のテレビをユーザーインターフェースとして活用できます。テレビは多くの家庭で日常的に使用されるため、特に高齢者にとっては親しみやすいデバイスです。そのため、新しいアプリケーションやシステムを導入する際のハードルを下げることができます。

**手軽な開発環境**: ChromecastはAndroidやiOS、Chromeブラウザと連携が可能なため、開発環境として手軽さを提供しています。既存の技術スタックを用いてアプリケーションを開発し、それをChromecastを通じてテレビにストリーミングすることができます。

**高い拡張性**: ChromecastはGoogleの広範で包括的なエコシステムの一部であり、Google Play MusicやYouTube、Netflixなどのアプリとも互換性があります。これにより、ユーザー体験の拡張や新たなサービスの開発が可能となります。

本プロジェクトにおいて、Chromecastはこのような利点を活かすことで、アプリケーションの開発と展開を円滑に進めることができました。開発者としては、このような優れたツールを用いて、より良いサービスを提供できることに感謝しています。

## 最後に
私の経験が皆さんの一助になれば幸いです。今回の経験から学んだことは、失敗自体が価値のある学びであるということです。これはエンジニアリングだけでなく、人生全般においても言えることだと思います。何か新しいことに挑戦した時、全てが順調に進むとは限りません。失敗することもあるでしょう。しかし、それらの失敗が、次にどのように行動するかを学ぶための貴重なフィードバックを提供してくれます。

失敗から得た知識や経験は、その後の成功に繋がる貴重な糧となります。今回、私が開発したアプリが直面した課題は、たしかに予期せぬものでした。しかし、その過程で得た洞察は、今後の開発や取り組みに大いに役立つと感じています。皆さんが同じ失敗をしないように、また、新たな挑戦を恐れずに前進するための助けとなるように、私のこの経験が役立つことを心から願っています。

私たちは全て学習者です。失敗はその一部であり、それを恐れることなく、それぞれの挑戦から学びを得て、成長していくことが大切です。これからも私たちは、技術の進歩とともに、日々新たな挑戦を続けていくでしょう。それぞれの経験、それが成功であれ失敗であれ、全てが我々を成長させ、より良い未来へと導いてくれるはずです。



# 開発したアプリについて
コード全文を書いておきますのでご自由にお使いください

## データ
assets/images/image1.png, ...（表示される画像）
assets/schedule.json (時間と画像とテキストのデータ)
```json
[
    
    {
        "at": "6:00",
        "image": "assets/images/image1.png",
        "text" : "起床時"
    },
...
```

## コード
```dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:intl/intl.dart';
import 'dart:convert';
import 'dart:async';
import 'package:keep_screen_on/keep_screen_on.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'dart:io';



void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Chromecast Image Switcher',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<Map<String, dynamic>> _schedule = [];
  late Timer _timer;
  FlutterTts flutterTts = FlutterTts();

  @override
  void initState() {
    super.initState();
    // Keep the screen on.
    if(!Platform.isMacOS) KeepScreenOn.turnOn();
    _loadSchedule();
    _startTimer();

    Future(() async {
      if (_isSleepingTime()) return; // Skip if it's sleeping time

      // ボイスの設定
      String voiceName = "";
      for (var item in await flutterTts.getVoices) {
        var map = item as Map<Object?, Object?>;
        if (map["locale"].toString().toLowerCase().contains("ja")) {
          debugPrint("${map["name"]}");
          voiceName = map["name"].toString();
        }
      }

      // 音声を固定にしている
      voiceName = "ja-jp-x-jab-network";

      if(Platform.isAndroid) await flutterTts.setQueueMode(1);
      await flutterTts.setVoice({
        'name': voiceName,
        'locale': 'ja-JP'
      });

      // 話す速度の設定
      await flutterTts.setPitch(0.9);
      await flutterTts.setSpeechRate(0.6);

      await flutterTts.speak("おはようございます。今日も一日頑張りましょう！");
    });
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  Future<void> _loadSchedule() async {
    String jsonString = await rootBundle.loadString('assets/schedule.json');
    List<dynamic> jsonDecoded = json.decode(jsonString);
    setState(() {
      for (var schedule in jsonDecoded) {
        _schedule.add(Map<String, dynamic>.from(schedule));
      }
    });
  }

  

  void _startTimer() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {});

      if (!_isSleepingTime() && DateFormat('mm:ss').format(DateTime.now()) == "00:00") {
        flutterTts.speak(DateFormat('HH時になりました').format(DateTime.now()));
      }
    });
  }

  bool _isSleepingTime() {
    final now = DateTime.now();
    final currentTimeInMinutes = now.hour * 60 + now.minute;
    const sleepTimeInMinutes = 21 * 60;
    const wakeupTimeInMinutes = 6 * 60;

    if (currentTimeInMinutes >= sleepTimeInMinutes || currentTimeInMinutes < wakeupTimeInMinutes) {
      return true;
    }
    return false;
  }

  String _currentImage() {
    final now = DateTime.now();
    final nowInMinutes = now.hour * 60 + now.minute;
    String currentImage = _schedule.first['image'];

    for (final schedule in _schedule) {
      final scheduleTime = schedule['at'].split(':');
      final scheduleInMinutes = int.parse(scheduleTime[0]) * 60 + int.parse(scheduleTime[1]);
      if (nowInMinutes >= scheduleInMinutes) {
        currentImage = schedule['image'];
      } else {
        break;
      }
    }
    return currentImage;
  }

  _getDateText(){

  var now = DateTime.now();


  const weekdayToJapanese = {
    1: '月曜日',
    2: '火曜日',
    3: '水曜日',
    4: '木曜日',
    5: '金曜日',
    6: '土曜日',
    7: '日曜日',
  };

  var weekDay = weekdayToJapanese[now.weekday] ?? 'Unknown';


  var reiwaYear = now.year - 2018; // 令和元年は2019年なので

  var formatter = DateFormat('MM月dd日 ($weekDay)\na hh時mm分');
  var formattedDate = formatter.format(now).replaceAll('AM', '午前').replaceAll('PM', '午後');

  return '令和$reiwaYear年$formattedDate';
  
}


  @override
  Widget build(BuildContext context) {
    String formattedTime = _getDateText();
    return Scaffold(
      body: Center(
        child: _schedule.isEmpty
            ? const CircularProgressIndicator()
            : _isSleepingTime()
                ? Container(
                    color: Colors.black,
                    child: const Center(
                      child: Icon(
                        Icons.nights_stay,
                        color: Colors.white,
                        size: 100.0,
                      ),
                    ),
                  )
                : Stack(
                    children: <Widget>[
                      Image.asset(
                        _currentImage(),
                        fit: BoxFit.cover,
                      ),
                      
                      Positioned(
                        top: 10.0,
                        left: 20.0,
                        child: Text(
                          formattedTime,
                          style: const TextStyle(
                            color: Colors.black,
                            fontSize: 24.0,
                          ),
                        ),
                      ),
                      const Positioned(
                        bottom: 20.0,
                        left: 0,
                        right: 0,
                        child: Center(child: Text(
                          "お薬は飲みましたか？",
                          style: TextStyle(
                            color: Colors.black,
                            fontSize: 24.0,
                          ),
                        )),
                      ),
                    ],
                  ),
      ),
    );
  }
}

```

## Chromecast へのデプロイ方法
「設定」→「システム」→「Android TV の OS ビルド」の上で数回決定ボタンを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167934/58835628-537a-6872-d47f-fcda4fbddfa4.png)

表示された「開発者向けオプション」の中から「USBデバッグ」を有効化。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167934/26d9a358-1222-f0dc-16e2-937b41ea53ff.png)

ChromecastのIPアドレス確認
「設定」→「ネットワークとインターネット」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167934/8959f29c-e911-fd1a-7bb5-bcd84ff1e644.png)

Chromecastと接続
```shell
adb connect 192.168.x.x
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167934/e738a8d7-3203-9b6c-65c5-9788a5f7e7f7.png)

ビルド → インストール →　起動
```shell
# ビルド
flutter build apk

# インストール
./adb -s 192.168.1.101:5555 install build/app/outputs/flutter-apk/app-release.apk

# 起動
./adb -s 192.168.x.x:5555 shell am start -n com.example.image_switcher/.MainActivity
```

# お礼
最後までお読み頂きましてありがとうございます。