🔨

【個人開発】メンテナンスページを作成した

2024/11/28に公開

背景

以前、DBのスキーマを変更したという記事を出しました。
https://zenn.dev/tnakano/articles/5a5e9d88e6e72c

この作業の一部として、メンテナンスページを作成したので、参考までに残しておきます。
個人開発において必須のページだと思います。

メンテナンスページとは?
今回はiosアプリで作成しましたが、webページ、ゲームでもよく見かけると思います。
DBのスキーマ変更や大きな修正時にユーザに触って欲しくない場合に使用します。

メンテナンスページの多用は、ユーザからすると使用したい時に使用できないので不満に繋がりやすいです。どうしても止める必要がある場合の奥の手として使用してください。

設計方針

Webアプリであれば、自由にデプロイできるので、以下で簡単に作れそうです。
①homeページをメンテナンスページにしてデプロイ
②作業が終わったら、ページを戻して再デプロイ

iosアプリはデプロイに審査が入るので、おそらく①のメンテナンスページだけのアプリは審査が通らないです。そこで、メンテナンスページの表示・非表示の切り替えがリアルタイムで出来ることを考慮しました。

実装

DBであれば、すぐに更新できるので、テーブルを作成しました。

configテーブル

id name flg
xxxxx maintenance false

APIにconfigテーブルを取得するエンドポイントを追加しました。

@router.get("/configs", response_model=List[config_schema.ConfigListResponse])
def get_all():
    return config_db.get_all()
def get_all():
    return list(Config.scan())

次はflutter側です。
メンテナンスチェック用のロジックを作成して、画面描画前に毎回チェックするようにしました。
一覧画面、登録画面、サインアップ画面に入れました。
とりあえず、登録や更新をさせたくなかったので、入り口を塞ぐ目的で画面選定しました。

最初はmain.dartに入れることも考えましたが、アプリ起動時にチェックしても、既にログイン済みのユーザはどうにもできないので不採用にしました。
調べれば、全画面共通で使用できるライブラリあるかもしれないです。

画面用


  void initState() {
    super.initState();

    Future.microtask(() async {
      await configService.checkMaintenanceStatus(false);
    });
  }

次はメンテナンスページの作成です。
メンテナンスページを開いた状態で、メンテナンスが終了した場合の考慮をしています。
アプリのバックグラウンド起動を削除して再起動すれば、ログイン画面に戻るのですが、そこが手間になると感じたので、5秒毎にフラグのチェックをAPIコールするようにしました。
ユーザからは特別な操作が必要なくなりました。

また、メンテナンスの詳細情報はNotionのリンクを貼って柔軟に文章を変更できるようにしてます。

MaintenancePage.dart

import 'package:flutter/cupertino.dart';
import 'package:url_launcher/url_launcher.dart';

import '../service/config_service.dart';

class MaintenancePage extends StatefulWidget {
  const MaintenancePage({Key? key}) : super(key: key);

  
  _MaintenancePageState createState() => _MaintenancePageState();
}

class _MaintenancePageState extends State<MaintenancePage> {
  late bool isMaintenance;

  
  void initState() {
    super.initState();

    Future.microtask(() async {
      final configService = ConfigService();

      Future.delayed(const Duration(seconds: 5), () async {
        await configService.checkMaintenanceStatus(true);
      });
    });
  }

  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              CupertinoIcons.alarm,
              color: CupertinoColors.activeOrange,
              size: 100,
            ),
            SizedBox(height: 20),
            Text(
              'ただいまメンテナンス中です。\n皆様にはご不便をおかけします。\nしばらくお待ちください。',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: CupertinoColors.activeOrange,
              ),
            ),
            SizedBox(height: 20),
            Text(
              '※この画面は5秒毎に切り替わります。',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
                color: CupertinoColors.secondaryLabel,
              ),
            ),
            SizedBox(height: 20),
            CupertinoButton(
              child: Text('詳細はこちら'),
              onPressed: () async {
                const url =
                    ''; //notionの公開ページを作成
                await launchUrl(Uri.parse(url));
              },
            ),
          ],
        ),
      ),
    );
  }
}

メンテナンスページとそれ以外のページで分けたかったので、分岐させてます。
メンテナンスページはメンテナンスが終わった場合は、ログインページに遷移させてます。
ConfigService.dart

Future<void> checkMaintenanceStatus(bool fromMaintenance) async {
    final response = await _getConfigs();
    final isMaintenance =
        response.where((it) => it.name == 'maintenance').first.flg;

    if (fromMaintenance) {
      if (isMaintenance) {
        navigatorKey.currentState!.pushAndRemoveUntil(
          CupertinoPageRoute(builder: (_) => const MaintenancePage()),
          (route) => false,
        );
      } else {
        navigatorKey.currentState!.pushAndRemoveUntil(
          CupertinoPageRoute(builder: (_) => LoginPage('config_service')),
          (route) => false,
        );
      }
    } else {
      if (isMaintenance) {
        navigatorKey.currentState!.pushAndRemoveUntil(
          CupertinoPageRoute(builder: (_) => const MaintenancePage()),
          (route) => false,
        );
      }
    }
  }

おわり

flutter自体は詳しくないのですが、全体的な設計は短時間で割と上手く行きました。
ただ、普段はcheckMaintenanceStatusをオフにしておこうかなと考えてます。
APIリクエストの数を増やしたくないし、メンテナンスは高頻度で起こるものではないので。

なるべくユーザが不満に持ちそうな点を先回りして、対策する癖がついてきた気がします。

正直リアルタイムで誰も使っていない時間は山ほどあるので、ここまでしなくても何とかなりましたが、万が一更新が起きた場合を考えて今回は作りました。

Discussion