Flutter/ShoreBird

Flutter ) ShoreBird 활용 강제 업데이트 적용

EEYatHo 2025. 4. 14. 16:46
반응형

0. 개요

Shorebird는 패치를 다운로드한 후 앱을 재시작하지 않으면 최신패치가 적용되지 않음

 

치명적인 오류를 OTA로 급히 수정했는데, 유저에게 즉시 패치된 버전을 제공할 수 없다는 점은 큰 단점
(특히, 앱 스토어에서 처음 다운로드시, 패치가 적용되지않은 초기 release 버전을 설치)

 

하지만, Shorebird 상태를 관리하고 강제로 재실행시키는 로직을 추가하는 등의 커스텀이 가능

 

shorebird에서 제공하는 자동 업데이트를 제거하고, 커스텀하게 조절하는 부분이 필요

 

 

 

1. 자동 업데이트 끄기

shorebird.yaml 에서 auto_update: false

// shorebird.yaml
auto_update: false

 

 

2. shorebird_code_push 라이브러리 적용

// pubspec.yaml
# shorebird 버전 파악 및 강제 업데이트 추가를 위함
shorebird_code_push: 2.0.2

 

 

3. ShoreBird 상태를 체크하고 패치를 다운로드하는 클래스 선언

특이사항

- 롤백하는 경우 대응하기 위해, upToDate 여도 다운로드 시도

import 'package:flutter/material.dart';
import 'package:shorebird_code_push/shorebird_code_push.dart';

final class ShorebirdService {
  final _updater = ShorebirdUpdater();
  Patch? currentPatch;
  bool _needRestart = false;

  Future<bool> isNeedRestart() async {
    _needRestart = false;
    if (!_updater.isAvailable) {
      debugPrint("🕊️🚨 updater.isAvailable == false 이므로 패스");
      return false;
    }
    await _readPatch();
    await _checkForUpdate();
    return _needRestart;
  }

  Future<void> _readPatch() async {
    try {
      final currentPatch = await _updater.readCurrentPatch();
      debugPrint("🕊️ 현재 패치 버전: ${currentPatch?.number}");
      this.currentPatch = currentPatch;
    } catch (error) {
      debugPrint("🕊️🚨 _readPatch Error: $error");
    }
  }

  Future<void> _checkForUpdate() async {
    try {
      debugPrint("🕊️ 업데이트 필요한지 체크");
      final status = await _updater.checkForUpdate(track: UpdateTrack.stable);

      switch (status) {
        case UpdateStatus.upToDate:
          debugPrint("🕊️ 최신버전이지만 롤백하는 경우도 있어서 시도해야함 (upToDate)");
          await _downloadAndUpdate();
        case UpdateStatus.outdated:
          debugPrint("🕊️ 패치 다운로드 필요 (outdated)");
          await _downloadAndUpdate();
        case UpdateStatus.restartRequired:
          debugPrint("🕊️ 재시작 필요 (restartRequired)");
          _needRestart = true;
        case UpdateStatus.unavailable:
          debugPrint("🕊️ 불가능 (unavailable)");
      }
    } catch (error) {
      debugPrint("🕊️🚨 checkForUpdate Error: $error");
    }
  }

  Future<void> _downloadAndUpdate() async {
    try {
      // 다운로드 및 업데이트 (_updater.update 완료시, 재시작하면 되는 상태가 됨)
      await _updater.update(track: UpdateTrack.stable);
      debugPrint("🕊️ 패치 다운로드 완료");
      _needRestart = true;
    } on UpdateException catch (error) {
      debugPrint("🕊️🚨 downloadPatch Error: ${error.message}");
    }
  }
}

 

 

4. 앱 시작시 체크

// iOS는 조금 오래걸리므로, await 사용 x
shorebirdService.isNeedRestart().then((isNeedRestart) {
  if (isNeedRestart) {
    showRestartDialog();
    return;
  }
});
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:homeknocktown_admin/presentation/design_system/atoms/dialog/dialog.dart';
import 'package:homeknocktown_admin/route.dart';
import 'package:homeknocktown_admin/utils/local_notification.dart';

Future<dynamic> showRestartDialog() async {
  return showDialog(
      context: navigatorKey.currentContext!,
      barrierDismissible: false, // 배경 눌러도 안꺼지게
      builder: (_) {
        return PopScope(
            canPop: false,
            child: YourAwesomeDialog( /// dialog 정도는 각자 알아서 띄워주기..
                text: "개선사항을 적용할 준비가 되었어요! 앱을 재시작 해주세요.",
                confirmText: "앱 종료",
                onConfirm: () async {
                  // await showLocalNotification(1, "개선사항 반영 완료", "앱을 다시 시작하기 위해 눌러주세요!");
                  exit(0);
                }));
      });
}
  • 보너스
    • iOS는 앱에서 스스로 재시작이 불가능함.
    • 로컬 푸시로 유저에게 재실행하기 좋은 UX 제공
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

// 로컬 푸시 알림 보여주기
Future<void> showLocalNotification(int id, String? title, String? body) async {
  debugPrint("🔔 showLocalNotification id: $id, title: $title, body: $body");

  await plugin.show(
    id,
    title,
    body,
    NotificationDetails(
      android: AndroidNotificationDetails(
        channel.id,
        channel.name,
        channelDescription: channel.description,
        importance: Importance.max,
        priority: Priority.high,
        icon: '@mipmap/ic_notification', // 알림 아이콘
        enableVibration: true, // 진동 활성화
        playSound: true, // 소리 활성화
        showWhen: true, // 시간 표시
      ),
    ),  );
}