Skip to content

Commit

Permalink
feat(shorebird_code_push): add download update functionality (#37)
Browse files Browse the repository at this point in the history
* feat: add shorebird_current_boot_patch_number function

* Continue support for next_patch_version

* rename, return null in case of 0

* feat(shorebird_code_push): add download update functionality

* rename defaultValue to fallbackValue

* add forTest constructor to ShorebirdCodePush
  • Loading branch information
bryanoltman authored Jun 20, 2023
1 parent 318d86e commit 62ed676
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 62 deletions.
27 changes: 25 additions & 2 deletions shorebird_code_push/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class _MyHomePageState extends State<MyHomePage> {
int? _currentPatchVersion;
int? _nextPatchVersion;
bool _isCheckingForUpdate = false;
bool _isUpdateAvailable = false;
bool _isDownloadingUpdate = false;

@override
void initState() {
Expand All @@ -57,7 +59,7 @@ class _MyHomePageState extends State<MyHomePage> {
_isCheckingForUpdate = true;
});

final isUpdateAvailable = await _shorebirdCodePush.checkForUpdate();
_isUpdateAvailable = await _shorebirdCodePush.checkForUpdate();

if (!mounted) return;

Expand All @@ -68,12 +70,26 @@ class _MyHomePageState extends State<MyHomePage> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
isUpdateAvailable ? 'Update available' : 'No update available',
_isUpdateAvailable ? 'Update available' : 'No update available',
),
),
);
}

Future<void> _downloadUpdate() async {
setState(() {
_isDownloadingUpdate = true;
});

await _shorebirdCodePush.downloadUpdate();
_nextPatchVersion = await _shorebirdCodePush.nextPatchNumber();

if (!mounted) return;
setState(() {
_isDownloadingUpdate = false;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
Expand Down Expand Up @@ -120,6 +136,13 @@ class _MyHomePageState extends State<MyHomePage> {
)
: const Text('Check for update'),
),
if (_isUpdateAvailable)
ElevatedButton(
onPressed: _isDownloadingUpdate ? null : _downloadUpdate,
child: Text(
_isDownloadingUpdate ? 'Downloading...' : 'Download update',
),
),
],
),
),
Expand Down
95 changes: 78 additions & 17 deletions shorebird_code_push/lib/src/shorebird_code_push.dart
Original file line number Diff line number Diff line change
@@ -1,49 +1,110 @@
import 'dart:isolate';

import 'package:meta/meta.dart';
import 'package:shorebird_code_push/src/updater.dart';

/// A logging function for errors arising from interacting with the native code.
///
/// Used to override the default behavior of using [print].
typedef ShorebirdLog = void Function(Object? object);

/// A function that constructs an [Updater] instance. Used for testing.
@visibleForTesting
typedef UpdaterBuilder = Updater Function();

/// {@template shorebird_code_push}
/// Get info about your Shorebird code push app.
/// {@endtemplate}
class ShorebirdCodePush {
/// {@macro shorebird_code_push}
ShorebirdCodePush({
Updater Function()? createUpdater, // for testing
}) : _createUpdater = createUpdater ?? Updater.new;
this.logError = print,
}) : _buildUpdater = Updater.new;

/// A test-only constructor that allows overriding the Updater constructor.
@visibleForTesting
ShorebirdCodePush.forTest({
required this.logError,
required UpdaterBuilder buildUpdater,
}) : _buildUpdater = buildUpdater;

/// Logs error messages arising from interacting with the native code.
///
/// Defaults to [print].
final ShorebirdLog logError;

final Updater Function() _createUpdater;
final UpdaterBuilder _buildUpdater;
static const _loggingPrefix = '[ShorebirdCodePush]';

/// Checks whether a new patch is available for download.
///
/// Runs in a separate isolate to avoid blocking the UI thread.
Future<bool> checkForUpdate() {
return _runInIsolate((updater) => updater.checkForUpdate());
return _runInIsolate(
(updater) => updater.checkForUpdate(),
fallbackValue: false,
);
}

/// The version of the currently-installed patch. Null if no patch is
/// installed (i.e., the app is running the release version).
Future<int?> currentPatchNumber() {
return _runInIsolate((updater) {
final patchNumber = updater.currentPatchNumber();
return patchNumber == 0 ? null : patchNumber;
});
return _runInIsolate(
(updater) {
final patchNumber = updater.currentPatchNumber();
return patchNumber == 0 ? null : patchNumber;
},
fallbackValue: null,
);
}

/// The version of the patch that will be run on the next app launch. If no
/// new patch has been downloaded, this will be the same as
/// [currentPatchNumber].
Future<int?> nextPatchNumber() {
return _runInIsolate((updater) {
final patchNumber = updater.nextPatchNumber();
return patchNumber == 0 ? null : patchNumber;
});
return _runInIsolate(
(updater) {
final patchNumber = updater.nextPatchNumber();
return patchNumber == 0 ? null : patchNumber;
},
fallbackValue: null,
);
}

/// Downloads the latest patch, if available.
Future<void> downloadUpdate() async {
await _runInIsolate(
(updater) => updater.downloadUpdate(),
fallbackValue: null,
);
}

void _logError(Object error) {
final logMessage = '$_loggingPrefix $error';
if (error is ArgumentError) {
// ffi function lookup failures manifest as ArgumentErrors.
logError(
'''
$logMessage
This is likely because you are not running with the Shorebird Flutter engine (that is, if you ran with `flutter run` instead of `shorebird run`).''',
);
} else {
logError(logMessage);
}
}

/// Creates an [Updater] in a separate isolate and runs the given function.
Future<T> _runInIsolate<T>(T Function(Updater updater) f) async {
return Isolate.run(() {
/// Creates an [Updater] in a separate isolate and runs the given function. If
/// an error occurs, the error is logged and [fallbackValue] is returned.
Future<T> _runInIsolate<T>(
T Function(Updater updater) f, {
required T fallbackValue,
}) async {
try {
// Create a new Updater in the new isolate.
return f(_createUpdater());
});
return await Isolate.run(() => f(_buildUpdater()));
} catch (error) {
_logError(error);
return fallbackValue;
}
}
}
31 changes: 8 additions & 23 deletions shorebird_code_push/lib/src/updater.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import 'package:meta/meta.dart';
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';

/// {@template updater}
/// A wrapper around the generated [UpdaterBindings] that translates ffi types
/// into easier to use Dart types.
/// A wrapper around the generated [UpdaterBindings] that, when necessary,
/// translates ffi types into easier to use Dart types.
/// {@endtemplate}
class Updater {
/// Creates an [Updater] instance using the currently loaded dynamic library.
Expand All @@ -18,30 +18,15 @@ class Updater {
static late UpdaterBindings bindings;

/// The currently active patch number.
int currentPatchNumber() {
try {
return bindings.shorebird_current_boot_patch_number();
} catch (e) {
return 0;
}
}
int currentPatchNumber() => bindings.shorebird_current_boot_patch_number();

/// Whether a new patch is available.
bool checkForUpdate() {
try {
return bindings.shorebird_check_for_update();
} catch (e) {
return false;
}
}
bool checkForUpdate() => bindings.shorebird_check_for_update();

/// The next patch number that will be loaded. Will be the same as
/// currentPatchNumber if no new patch is available.
int nextPatchNumber() {
try {
return bindings.shorebird_next_boot_patch_number();
} catch (e) {
return 0;
}
}
int nextPatchNumber() => bindings.shorebird_next_boot_patch_number();

/// Downloads the latest patch, if available.
void downloadUpdate() => bindings.shorebird_update();
}
45 changes: 43 additions & 2 deletions shorebird_code_push/test/src/shorebird_code_push_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,88 @@ void main() {
group('ShorebirdCodePush', () {
late Updater updater;
late ShorebirdCodePush shorebirdCodePush;
Object? loggedError;

setUp(() {
loggedError = null;
updater = _MockUpdater();
shorebirdCodePush = ShorebirdCodePush(
createUpdater: () => updater,
shorebirdCodePush = ShorebirdCodePush.forTest(
logError: ([object]) => loggedError = object,
buildUpdater: () => updater,
);
});

group('checkForUpdate', () {
test('returns false if no update is available', () async {
when(() => updater.checkForUpdate()).thenAnswer((_) => false);
expect(await shorebirdCodePush.checkForUpdate(), isFalse);
expect(loggedError, isNull);
});

test('returns true if an update is available', () async {
when(() => updater.checkForUpdate()).thenAnswer((_) => true);
expect(await shorebirdCodePush.checkForUpdate(), true);
expect(loggedError, isNull);
});

test('returns false if updater throws exception', () async {
when(() => updater.checkForUpdate()).thenThrow(Exception('oh no'));
expect(await shorebirdCodePush.checkForUpdate(), isFalse);
expect(loggedError, '[ShorebirdCodePush] Exception: oh no');
});
});

group('currentPatchNumber', () {
test('returns null if current patch is reported as 0', () async {
when(() => updater.currentPatchNumber()).thenReturn(0);
expect(await shorebirdCodePush.currentPatchNumber(), isNull);
expect(loggedError, isNull);
});

test('forwards the return value of updater.currentPatchNumber', () async {
when(() => updater.currentPatchNumber()).thenReturn(1);
expect(await shorebirdCodePush.currentPatchNumber(), 1);
expect(loggedError, isNull);
});

test('returns null if updater throws exception', () async {
when(() => updater.currentPatchNumber()).thenThrow(Exception('oh no'));
expect(await shorebirdCodePush.currentPatchNumber(), isNull);
expect(loggedError, '[ShorebirdCodePush] Exception: oh no');
});
});

group('nextPatchNumber', () {
test('returns null if current patch is reported as 0', () async {
when(() => updater.nextPatchNumber()).thenReturn(0);
expect(await shorebirdCodePush.nextPatchNumber(), isNull);
expect(loggedError, isNull);
});

test('forwards the return value of updater.nextPatchNumber', () async {
when(() => updater.nextPatchNumber()).thenReturn(1);
expect(await shorebirdCodePush.nextPatchNumber(), 1);
expect(loggedError, isNull);
});

test('returns null if updater throws exception', () async {
when(() => updater.nextPatchNumber()).thenThrow(Exception('oh no'));
expect(await shorebirdCodePush.nextPatchNumber(), isNull);
expect(loggedError, '[ShorebirdCodePush] Exception: oh no');
});
});

group('downloadUpdate', () {
test('forwards the return value of updater.nextPatchNumber', () async {
when(() => updater.downloadUpdate()).thenReturn(null);
await expectLater(shorebirdCodePush.downloadUpdate(), completes);
expect(loggedError, isNull);
});

test('logs error if updater throws exception', () async {
when(() => updater.downloadUpdate()).thenThrow(Exception('oh no'));
await expectLater(shorebirdCodePush.downloadUpdate(), completes);
expect(loggedError, '[ShorebirdCodePush] Exception: oh no');
});
});
});
Expand Down
26 changes: 8 additions & 18 deletions shorebird_code_push/test/src/updater_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ void main() {
});

group('currentPatchNumber', () {
test('returns 0 if shorebird_next_boot_patch_number throws', () {
when(() => updaterBindings.shorebird_current_boot_patch_number())
.thenThrow(Exception());
expect(updater.currentPatchNumber(), 0);
});

test('forwards the result of shorebird_next_boot_patch_number', () {
when(() => updaterBindings.shorebird_current_boot_patch_number())
.thenReturn(123);
Expand All @@ -37,12 +31,6 @@ void main() {
});

group('checkForUpdate', () {
test('returns false if shorebird_check_for_update throws', () {
when(() => updaterBindings.shorebird_check_for_update())
.thenThrow(Exception());
expect(updater.checkForUpdate(), isFalse);
});

test('forwards the result of shorebird_check_for_update', () {
when(() => updaterBindings.shorebird_check_for_update())
.thenReturn(true);
Expand All @@ -55,18 +43,20 @@ void main() {
});

group('nextPatchNumber', () {
test('returns 0 if shorebird_next_boot_patch_number throws', () {
when(() => updaterBindings.shorebird_next_boot_patch_number())
.thenThrow(Exception());
expect(updater.nextPatchNumber(), 0);
});

test('forwards the result of shorebird_next_boot_patch_number', () {
when(() => updaterBindings.shorebird_next_boot_patch_number())
.thenReturn(123);
final currentPatchNumber = updater.nextPatchNumber();
expect(currentPatchNumber, 123);
});
});

group('downloadUpdate', () {
test('calls bindings.shorebird_update', () {
when(() => updaterBindings.shorebird_update()).thenReturn(null);
updater.downloadUpdate();
verify(() => updaterBindings.shorebird_update()).called(1);
});
});
});
}

0 comments on commit 62ed676

Please sign in to comment.