ee3f5ec669
* feat(shorebird_code_push): track support * cleanup and add todos * docs * run ffigen * use c_char instead of char * Add channel support * tests * tests * update podfile.lock * Update example to include tracks selector --------- Co-authored-by: Bryan Oltman <bryan@shorebird.dev> Co-authored-by: Bryan Oltman <bryanoltman@gmail.com>
328 lines
9.0 KiB
Dart
328 lines
9.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
|
|
|
void main() => runApp(const MyApp());
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Shorebird Code Push Demo',
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
|
|
useMaterial3: true,
|
|
),
|
|
home: const MyHomePage(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MyHomePage extends StatefulWidget {
|
|
const MyHomePage({super.key});
|
|
|
|
@override
|
|
State<MyHomePage> createState() => _MyHomePageState();
|
|
}
|
|
|
|
class _MyHomePageState extends State<MyHomePage> {
|
|
final _updater = ShorebirdUpdater();
|
|
late final bool _isUpdaterAvailable;
|
|
var _currentTrack = UpdateTrack.stable;
|
|
var _isCheckingForUpdates = false;
|
|
Patch? _currentPatch;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Check whether Shorebird is available.
|
|
setState(() => _isUpdaterAvailable = _updater.isAvailable);
|
|
|
|
// Read the current patch (if there is one.)
|
|
// `currentPatch` will be `null` if no patch is installed.
|
|
_updater.readCurrentPatch().then((currentPatch) {
|
|
setState(() => _currentPatch = currentPatch);
|
|
}).catchError((Object error) {
|
|
// If an error occurs, we log it for now.
|
|
debugPrint('Error reading current patch: $error');
|
|
});
|
|
}
|
|
|
|
Future<void> _checkForUpdate() async {
|
|
if (_isCheckingForUpdates) return;
|
|
|
|
try {
|
|
setState(() => _isCheckingForUpdates = true);
|
|
// Check if there's an update available.
|
|
final status = await _updater.checkForUpdate(track: _currentTrack);
|
|
if (!mounted) return;
|
|
// If there is an update available, show a banner.
|
|
switch (status) {
|
|
case UpdateStatus.upToDate:
|
|
_showNoUpdateAvailableBanner();
|
|
case UpdateStatus.outdated:
|
|
_showUpdateAvailableBanner();
|
|
case UpdateStatus.restartRequired:
|
|
_showRestartBanner();
|
|
case UpdateStatus.unavailable:
|
|
// Do nothing, there is already a warning displayed at the top of the
|
|
// screen.
|
|
}
|
|
} catch (error) {
|
|
// If an error occurs, we log it for now.
|
|
debugPrint('Error checking for update: $error');
|
|
} finally {
|
|
setState(() => _isCheckingForUpdates = false);
|
|
}
|
|
}
|
|
|
|
void _showDownloadingBanner() {
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentMaterialBanner()
|
|
..showMaterialBanner(
|
|
const MaterialBanner(
|
|
content: Text('Downloading...'),
|
|
actions: [
|
|
SizedBox(
|
|
height: 14,
|
|
width: 14,
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showUpdateAvailableBanner() {
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentMaterialBanner()
|
|
..showMaterialBanner(
|
|
MaterialBanner(
|
|
content: Text(
|
|
'Update available for the ${_currentTrack.name} track.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () async {
|
|
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
|
await _downloadUpdate();
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
|
},
|
|
child: const Text('Download'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showNoUpdateAvailableBanner() {
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentMaterialBanner()
|
|
..showMaterialBanner(
|
|
MaterialBanner(
|
|
content: Text(
|
|
'No update available on the ${_currentTrack.name} track.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
|
},
|
|
child: const Text('Dismiss'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showRestartBanner() {
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentMaterialBanner()
|
|
..showMaterialBanner(
|
|
MaterialBanner(
|
|
content: const Text('A new patch is ready! Please restart your app.'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
|
},
|
|
child: const Text('Dismiss'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showErrorBanner(Object error) {
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentMaterialBanner()
|
|
..showMaterialBanner(
|
|
MaterialBanner(
|
|
content: Text(
|
|
'An error occurred while downloading the update: $error.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
|
},
|
|
child: const Text('Dismiss'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _downloadUpdate() async {
|
|
_showDownloadingBanner();
|
|
try {
|
|
// Perform the update (e.g download the latest patch on [_currentTrack]).
|
|
// Note that [track] is optional. Not passing it will default to the
|
|
// stable track.
|
|
await _updater.update(track: _currentTrack);
|
|
if (!mounted) return;
|
|
// Show a banner to inform the user that the update is ready and that they
|
|
// need to restart the app.
|
|
_showRestartBanner();
|
|
} on UpdateException catch (error) {
|
|
// If an error occurs, we show a banner with the error message.
|
|
_showErrorBanner(error.message);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: theme.colorScheme.inversePrimary,
|
|
title: const Text('Shorebird Code Push'),
|
|
),
|
|
body: Column(
|
|
children: [
|
|
if (!_isUpdaterAvailable) const _ShorebirdUnavailable(),
|
|
const Spacer(),
|
|
_CurrentPatchVersion(patch: _currentPatch),
|
|
const SizedBox(height: 12),
|
|
_TrackPicker(
|
|
currentTrack: _currentTrack,
|
|
onChanged: (track) {
|
|
setState(() => _currentTrack = track);
|
|
},
|
|
),
|
|
const Spacer(),
|
|
],
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
onPressed: _isCheckingForUpdates ? null : _checkForUpdate,
|
|
tooltip: 'Check for update',
|
|
child: _isCheckingForUpdates
|
|
? const _LoadingIndicator()
|
|
: const Icon(Icons.refresh),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Widget that is mounted when Shorebird is not available.
|
|
class _ShorebirdUnavailable extends StatelessWidget {
|
|
const _ShorebirdUnavailable();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Center(
|
|
child: Text(
|
|
'''
|
|
Shorebird is not available.
|
|
Please make sure the app was generated via `shorebird release` and that it is running in release mode.''',
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
color: theme.colorScheme.error,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Widget that displays the current patch version.
|
|
class _CurrentPatchVersion extends StatelessWidget {
|
|
const _CurrentPatchVersion({required this.patch});
|
|
|
|
final Patch? patch;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
const Text('Current patch version:'),
|
|
Text(
|
|
patch != null ? '${patch!.number}' : 'No patch installed',
|
|
style: theme.textTheme.headlineMedium,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Widget that allows selection of update track.
|
|
class _TrackPicker extends StatelessWidget {
|
|
const _TrackPicker({
|
|
required this.currentTrack,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final UpdateTrack currentTrack;
|
|
|
|
final ValueChanged<UpdateTrack> onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
const Text('Update track:'),
|
|
SegmentedButton<UpdateTrack>(
|
|
segments: const [
|
|
ButtonSegment(
|
|
label: Text('Stable'),
|
|
value: UpdateTrack.stable,
|
|
),
|
|
ButtonSegment(
|
|
label: Text('Beta'),
|
|
icon: Icon(Icons.science),
|
|
value: UpdateTrack.beta,
|
|
),
|
|
ButtonSegment(
|
|
label: Text('Staging'),
|
|
icon: Icon(Icons.construction),
|
|
value: UpdateTrack.staging,
|
|
),
|
|
],
|
|
selected: {currentTrack},
|
|
onSelectionChanged: (tracks) => onChanged(tracks.single),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A reusable loading indicator.
|
|
class _LoadingIndicator extends StatelessWidget {
|
|
const _LoadingIndicator();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const SizedBox(
|
|
height: 14,
|
|
width: 14,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
);
|
|
}
|
|
}
|