feat(shorebird_code_push): add updater bindings and dart support (#32)
* feat(shorebird_code_push): add updater bindings and dart support * newlines * docs * Remove unused support for dart cli * clarify Android-specific setup in readme
This commit is contained in:
@@ -4,6 +4,38 @@
|
||||
|
||||
A Dart library that allows Flutter apps to get information about code push updates.
|
||||
|
||||
## Usage
|
||||
|
||||
```dart
|
||||
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
||||
|
||||
final shorebirdCodePush = ShorebirdCodePush();
|
||||
|
||||
// Gets the current patch version, or null if no patch is installed.
|
||||
final currentPatchversion = shorebirdCodePush.currentPatchVersion();
|
||||
|
||||
// Checks whether a patch is available to install.
|
||||
final isUpdateAvailable = await shorebirdCodePush.checkForUpdate();
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
### FFI
|
||||
|
||||
The Dart code in this library communicates with the Updater (part of Shorebird's
|
||||
Flutter engine) via FFI.
|
||||
|
||||
For an Updater function to be visible to the Dart code, it must:
|
||||
|
||||
1. Be declared in c_api.rs as `pub extern "C"`.
|
||||
1. This will add the function to the `library/include/updater.h` header
|
||||
file, which is generated by [cbindgen](https://github.com/mozilla/cbindgen)
|
||||
when the Updater is built.
|
||||
1. Be included in the generated ffi bindings. These can be regenerated using
|
||||
`dart run ffigen`.
|
||||
1. Android specific: be listed in
|
||||
https://github.com/shorebirdtech/engine/blob/main/shell/platform/android/android_exports.lst
|
||||
|
||||
|
||||
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[license_link]: https://opensource.org/licenses/MIT
|
||||
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
include: package:very_good_analysis/analysis_options.5.0.0.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/**.g.dart
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,36 @@
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
|
||||
/// {@template shorebird_code_push}
|
||||
/// Get info about your Shorebird code push app
|
||||
/// Get info about your Shorebird code push app.
|
||||
/// {@endtemplate}
|
||||
class ShorebirdCodePush {
|
||||
/// {@macro shorebird_code_push}
|
||||
const ShorebirdCodePush();
|
||||
ShorebirdCodePush({
|
||||
Updater Function()? createUpdater, // for testing
|
||||
}) : _createUpdater = createUpdater ?? Updater.new;
|
||||
|
||||
final Updater Function() _createUpdater;
|
||||
|
||||
/// 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());
|
||||
}
|
||||
|
||||
/// The version of the currently-installed patch. Null if no patch is
|
||||
/// installed (i.e., the app is running the release version).
|
||||
Future<int?> currentPatchVersion() {
|
||||
return _runInIsolate((updater) => updater.currentPatchNumber());
|
||||
}
|
||||
|
||||
/// 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(() {
|
||||
// Create a new Updater in the new isolate.
|
||||
return f(_createUpdater());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'dart:ffi' as ffi;
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
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.
|
||||
/// {@endtemplate}
|
||||
class Updater {
|
||||
/// Creates an [Updater] instance using the currently loaded dynamic library.
|
||||
Updater() {
|
||||
bindings = UpdaterBindings(ffi.DynamicLibrary.process());
|
||||
}
|
||||
|
||||
/// The ffi bindings to the Updater library.
|
||||
@visibleForTesting
|
||||
static late UpdaterBindings bindings;
|
||||
|
||||
/// The currently active patch number.
|
||||
// TODO(bryanoltman): this will return the current number + 1 if an update is
|
||||
// available. It should instead always return the current patch version.
|
||||
int? currentPatchNumber() {
|
||||
final patchNumberString = _returnsMaybeString(
|
||||
bindings.shorebird_next_boot_patch_number,
|
||||
);
|
||||
return patchNumberString == null ? null : int.tryParse(patchNumberString);
|
||||
}
|
||||
|
||||
/// Whether a new patch is available.
|
||||
bool checkForUpdate() => bindings.shorebird_check_for_update();
|
||||
|
||||
/// A wrapper for ffi functions that return [Pointer<Char].
|
||||
String? _returnsMaybeString(ffi.Pointer<ffi.Char> Function() f) {
|
||||
final cString = f();
|
||||
if (cString.address == ffi.nullptr.address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final utf8Pointer = cString.cast<Utf8>();
|
||||
|
||||
try {
|
||||
return utf8Pointer.toDartString();
|
||||
} finally {
|
||||
// Using finally for two reasons:
|
||||
// 1. it runs after the return (saving us a local)
|
||||
// 2. it runs even if toDartString throws (which it shouldn't)
|
||||
bindings.shorebird_free_string(cString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,19 @@ version: 0.1.0+1
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
ffi: ^2.0.2
|
||||
meta: ^1.9.1
|
||||
|
||||
dev_dependencies:
|
||||
ffigen: ^8.0.2
|
||||
mocktail: ^0.3.0
|
||||
test: ^1.19.2
|
||||
very_good_analysis: ^5.0.0
|
||||
|
||||
ffigen:
|
||||
output: 'lib/src/generated/updater_bindings.g.dart'
|
||||
name: 'UpdaterBindings'
|
||||
headers:
|
||||
entry-points:
|
||||
- '../library/include/updater.h'
|
||||
@@ -1,11 +1,44 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockUpdater extends Mock implements Updater {}
|
||||
|
||||
void main() {
|
||||
group('ShorebirdCodePush', () {
|
||||
test('can be instantiated', () {
|
||||
expect(ShorebirdCodePush(), isNotNull);
|
||||
late Updater updater;
|
||||
late ShorebirdCodePush shorebirdCodePush;
|
||||
|
||||
setUp(() {
|
||||
updater = _MockUpdater();
|
||||
shorebirdCodePush = ShorebirdCodePush(
|
||||
createUpdater: () => updater,
|
||||
);
|
||||
});
|
||||
|
||||
group('checkForUpdate', () {
|
||||
test('returns false if no update is available', () async {
|
||||
when(() => updater.checkForUpdate()).thenAnswer((_) => false);
|
||||
expect(await shorebirdCodePush.checkForUpdate(), isFalse);
|
||||
});
|
||||
|
||||
test('returns true if an update is available', () async {
|
||||
when(() => updater.checkForUpdate()).thenAnswer((_) => true);
|
||||
expect(await shorebirdCodePush.checkForUpdate(), true);
|
||||
});
|
||||
});
|
||||
|
||||
group('currentPatchNumber', () {
|
||||
test('forwards the return value of updater.currentPatchNumber', () async {
|
||||
when(() => updater.currentPatchNumber()).thenReturn(1);
|
||||
expect(await shorebirdCodePush.currentPatchVersion(), 1);
|
||||
|
||||
when(() => updater.currentPatchNumber()).thenReturn(null);
|
||||
expect(await shorebirdCodePush.currentPatchVersion(), null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'dart:ffi' as ffi;
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockUpdaterBindings extends Mock implements UpdaterBindings {}
|
||||
|
||||
void main() {
|
||||
group(Updater, () {
|
||||
late UpdaterBindings updaterBindings;
|
||||
late Updater updater;
|
||||
|
||||
setUp(() {
|
||||
updaterBindings = _MockUpdaterBindings();
|
||||
|
||||
updater = Updater();
|
||||
Updater.bindings = updaterBindings;
|
||||
});
|
||||
|
||||
test('initializes from currently loaded library', () {
|
||||
expect(updater, isNotNull);
|
||||
});
|
||||
|
||||
group('currentPatchNumber', () {
|
||||
test('returns null if bindings return null pointer', () {
|
||||
when(() => updaterBindings.shorebird_next_boot_patch_number())
|
||||
.thenReturn(ffi.nullptr);
|
||||
final currentPatchNumber = updater.currentPatchNumber();
|
||||
expect(currentPatchNumber, isNull);
|
||||
});
|
||||
|
||||
test('returns number if bindings return non-null pointer', () {
|
||||
final charPtr = '123'.toNativeUtf8().cast<ffi.Char>();
|
||||
when(() => updaterBindings.shorebird_next_boot_patch_number())
|
||||
.thenReturn(charPtr);
|
||||
final currentPatchNumber = updater.currentPatchNumber();
|
||||
expect(currentPatchNumber, 123);
|
||||
});
|
||||
});
|
||||
|
||||
group('checkForUpdate', () {
|
||||
test('forwards the result of shorebird_check_for_update', () {
|
||||
when(() => updaterBindings.shorebird_check_for_update())
|
||||
.thenReturn(true);
|
||||
expect(updater.checkForUpdate(), isTrue);
|
||||
|
||||
when(() => updaterBindings.shorebird_check_for_update())
|
||||
.thenReturn(false);
|
||||
expect(updater.checkForUpdate(), isFalse);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user