Shorebird에서 제공하는 Flutter 앱을 OTA(Over-the-Air) 방식으로 코드 패치할 수 있게 해주는 공식 패키지
Shorebird엔진이 내장된 앱 실행
-> 새로운 패치가 있으면 다운로드 받음
-> 다음 앱 실행시 다운로드 받은 업데이트를 적용
즉, 패치 배포 이후 두번째 앱 실행시 새로운 패치가 적용되는 구조이다.
패치 배포 이후 첫번째 앱 실행시 바로 앱을 종료시키고 재시작 할 수 있도록 팝업을 추가하려고 한다.
@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: 현재 적용된 패치 정보를 저장.
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);
});
...