Shorebird 업데이트 팝업 적용

메모하는 개발자·2025년 7월 18일
0

Flutter메모

목록 보기
10/10

Shorebird Code Push

Shorebird에서 제공하는 Flutter 앱을 OTA(Over-the-Air) 방식으로 코드 패치할 수 있게 해주는 공식 패키지

Shorebird의 기본 동작

Shorebird엔진이 내장된 앱 실행
-> 새로운 패치가 있으면 다운로드 받음
-> 다음 앱 실행시 다운로드 받은 업데이트를 적용
즉, 패치 배포 이후 두번째 앱 실행시 새로운 패치가 적용되는 구조이다.

패치 배포 이후 첫번째 앱 실행시 바로 앱을 종료시키고 재시작 할 수 있도록 팝업을 추가하려고 한다.

1. ShorebirdService 클래스 생성


@lazySingleton
class ShorebirdService extends ChangeNotifier {
  final _updater = ShorebirdUpdater();
  Patch? currentPatch;
  
  UpdateStatus? _updateType;
  UpdateStatus? get updateType => _updateType;

  String _snackBarMessage = '';
  String get snackBarMessage => _snackBarMessage;

  set snackBarMessage(String message) {
    _snackBarMessage = message;
    notifyListeners();
  }

  Future<void> checkUpdate() async {
    if (!_updater.isAvailable) {
      _updateType = UpdateStatus.unavailable;
      snackBarMessage = 'Shorebird를 사용할 수 없습니다.';
      notifyListeners();
    }
    await _checkForUpdate();
  }


  Future<void> _checkForUpdate() async {
    final status = await _updater.checkForUpdate();
    _updateType = status;
    notifyListeners();
  }

  Future<void> downloadPatch() async {
    try {
      await _updater.update(track: UpdateTrack.stable);
      _updateType = UpdateStatus.restartRequired;
    } on UpdateException catch (error) {
      _updateType = UpdateStatus.unavailable;
    } finally {
      notifyListeners();
    }
  }
}

📦 주요 필드 설명
ShorebirdUpdater _updater: Shorebird에서 제공하는 핵심 유틸로, 업데이트 관련 기능을 담당.

UpdateStatus? _updateType: 현재 Shorebird 업데이트 상태를 저장.
ex) 최신, 다운로드 필요, 재시작 필요 등.

Patch? currentPatch: 현재 적용된 패치 정보를 저장.

2. Splash Screen 적용

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

  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  bool _dialogShown = false;
  UpdateStatus? _lastStatus;

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

    // Shorebird 업데이트 체크
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      final shorebirdService = context.read<ShorebirdService>();
      await shorebirdService.checkUpdate();
    });
  }

  Future<void> _handleUpdateStatus(
    ShorebirdService shorebirdService,
    SplashViewmodel viewModel,
  ) async {
    final currentStatus = shorebirdService.updateType;

    // 1. 상태 변화 시 스낵바 출력
    if (_lastStatus != currentStatus && currentStatus != null) {
      _lastStatus = currentStatus;

      final snackBarText = _statusToSnackBarText(currentStatus);
      if (snackBarText != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(snackBarText),
            duration: const Duration(seconds: 1),
          ),
        );
      }
    }

    // 2. 다이얼로그 띄우기 (다운로드 필요, 재시작 필요)
    if (!_dialogShown &&
        currentStatus != null &&
        (currentStatus == UpdateStatus.outdated ||
            currentStatus == UpdateStatus.restartRequired)) {
      _dialogShown = true;
      _showUpdateDialog(currentStatus, shorebirdService);
      return;
    }

    // 3. 로그인 시도
    if (currentStatus == UpdateStatus.upToDate ||
        currentStatus == UpdateStatus.unavailable) {
      await viewModel.autoLogin();
      if (mounted) _route(context, viewModel);
    }
  }

  String? _statusToSnackBarText(UpdateStatus status) {
    switch (status) {
      case UpdateStatus.upToDate:
        return '앱이 최신 버전입니다.';
      case UpdateStatus.outdated:
        return '새로운 업데이트가 있습니다.';
      case UpdateStatus.restartRequired:
        return '다운로드가 완료되었습니다. 재시작이 필요합니다.';
      case UpdateStatus.unavailable:
        return '업데이트 상태를 확인할 수 없습니다.';
    }
  }

  void _showUpdateDialog(UpdateStatus status, ShorebirdService service) {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder:
          (context) => AlertDialog(
            title: Text(
              status == UpdateStatus.restartRequired ? '다운로드 완료' : '다운로드 필요',
            ),
            content: Text(
              status == UpdateStatus.restartRequired
                  ? '다운로드가 완료되었습니다. 앱을 재시작하여 업데이트를 적용해주세요.'
                  : '신규 업데이트가 있습니다. 다운로드가 필요합니다.',
            ),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                  _dialogShown = false;

                  if (status == UpdateStatus.restartRequired) {
                    // methodChannel을 사용하여 앱 종료시키는 기능 추가
                  } else {
                    service.downloadPatch();
                  }
                },
                child: Text(
                  status == UpdateStatus.restartRequired ? '앱 종료' : '다운로드',
                ),
              ),
            ],
          ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Consumer2<SplashViewmodel, ShorebirdService>(
      builder: (context, viewModel, shorebirdService, child) {
        WidgetsBinding.instance.addPostFrameCallback((_) {
          _handleUpdateStatus(shorebirdService, viewModel);
        });
        ...

0개의 댓글