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:
Bryan Oltman
2023-06-19 20:53:51 -04:00
committed by GitHub
parent a593540f96
commit ea48f6cb4f
8 changed files with 4643 additions and 6 deletions
+33 -1
View File
@@ -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());
});
}
}
+52
View File
@@ -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);
}
}
}
+10
View File
@@ -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);
});
});
});
}