clean up
diff --git a/pkgs/hooks/test/update_snippets_test.dart b/pkgs/hooks/test/update_snippets_test.dart new file mode 100644 index 0000000..25b8292 --- /dev/null +++ b/pkgs/hooks/test/update_snippets_test.dart
@@ -0,0 +1,165 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:test/test.dart'; + +import '../tool/update_snippets.dart'; + +void main() { + late Directory tempDir; + late File snippetFile; + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('update_snippets_test'); + snippetFile = File.fromUri(tempDir.uri.resolve('snippet.dart')); + await snippetFile.writeAsString(''' +// snippet-start#my_snippet +void hello() { + print('hello'); +} +// snippet-end#my_snippet +'''); + }); + + tearDown(() async { + await tempDir.delete(recursive: true); + }); + + const tag = + '<!-- ' + 'file://./snippet.dart#my_snippet -->'; + const multilineTag = + '<!-- ' + 'file://./snippet_multiline.dart#my_snippet -->'; + const fence = + '```' + 'dart'; + const endFence = '```'; + + test('ordinary code block', () { + const docContent = + ''' +$tag +$fence +old content +$endFence +'''; + final docUri = tempDir.uri.resolve('doc.md'); + final errors = <String>[]; + final updated = updateSnippets(docContent, docUri, errors); + expect(errors, isEmpty); + expect(updated, ''' +$tag +$fence +void hello() { + print('hello'); +} +$endFence +'''); + }); + + test('doc comments (/// )', () { + const docContent = + ''' +/// $tag +/// $fence +/// old content +/// $endFence +'''; + final docUri = tempDir.uri.resolve('doc.dart'); + final errors = <String>[]; + final updated = updateSnippets(docContent, docUri, errors); + expect(errors, isEmpty); + expect(updated, ''' +/// $tag +/// $fence +/// void hello() { +/// print('hello'); +/// } +/// $endFence +'''); + }); + + test('blockquoted code block (> )', () { + const docContent = + ''' +> $tag +> $fence +> old content +> $endFence +'''; + final docUri = tempDir.uri.resolve('doc.md'); + final errors = <String>[]; + final updated = updateSnippets(docContent, docUri, errors); + expect(errors, isEmpty); + expect(updated, ''' +> $tag +> $fence +> void hello() { +> print('hello'); +> } +> $endFence +'''); + }); + + test('blockquoted code block without space (>```dart)', () { + const docContent = + ''' +>$tag +>$fence +>old content +>$endFence +'''; + final docUri = tempDir.uri.resolve('doc.md'); + final errors = <String>[]; + final updated = updateSnippets(docContent, docUri, errors); + expect(errors, isEmpty); + expect(updated, ''' +>$tag +>$fence +>void hello() { +> print('hello'); +>} +>$endFence +'''); + }); + + test('blockquoted code block with empty lines', () { + const docContent = + ''' +> $multilineTag +> $fence +> old content +> $endFence +'''; + final snippetMultiline = File.fromUri( + tempDir.uri.resolve('snippet_multiline.dart'), + ); + snippetMultiline.writeAsStringSync(''' +// snippet-start#my_snippet +void hello() { + print('hello'); + + print('world'); +} +// snippet-end#my_snippet +'''); + final docUri = tempDir.uri.resolve('doc.md'); + final errors = <String>[]; + final updated = updateSnippets(docContent, docUri, errors); + expect(errors, isEmpty); + expect(updated, ''' +> $multilineTag +> $fence +> void hello() { +> print('hello'); +> +> print('world'); +> } +> $endFence +'''); + }); +}
diff --git a/pkgs/hooks/tool/update_snippets.dart b/pkgs/hooks/tool/update_snippets.dart index 97ceeb3..e843f97 100644 --- a/pkgs/hooks/tool/update_snippets.dart +++ b/pkgs/hooks/tool/update_snippets.dart
@@ -91,7 +91,7 @@ var newContent = oldContent; final markers = RegExp( - r'^([ \t]*(?:/{3}[ ]?)?)```(\w*)', + r'^([ \t]*(?:(?:>[ \t]*)+|/{3}[ ]?)?)```(\w*)', multiLine: true, ).allMatches(oldContent); @@ -153,7 +153,7 @@ final lineBeforeText = oldContent.split('\n')[lastLineOfContentBefore]; final fileLineMatch = RegExp( - r'^(.*?)<!-- (?:file://./(\S+?)(?:#(\S+))?|(no-source-file)) -->\s*$', + r'^([ \t]*(?:(?:>[ \t]*)+|/{3}[ ]?)?)<!-- (?:file://./(\S+?)(?:#(\S+))?|(no-source-file)) -->\s*$', ).firstMatch(lineBeforeText); if (fileLineMatch == null) {
diff --git a/pkgs/jnigen/README.md b/pkgs/jnigen/README.md index a5ca5de..2d37533 100644 --- a/pkgs/jnigen/README.md +++ b/pkgs/jnigen/README.md
@@ -71,13 +71,16 @@ final packageRoot = Platform.script.resolve('../'); final generator = JniGenerator( input: Input( - sourcePath: [packageRoot.resolve('android/app/src/main/java')], + // Required. List of classes or packages for which bindings should be generated. classes: [ 'com.example.in_app_java', // Generate the entire package 'androidx.emoji2.text.EmojiCompat', // From gradle's compile classpath 'androidx.emoji2.text.DefaultEmojiCompatConfig', // From gradle's compile classpath 'android.os.Build', // from gradle's compile classpath ], + // Optional. List of directories that contain the source files for which to generate bindings. + sourcePath: [packageRoot.resolve('android/app/src/main/java')], + // Optional. Configuration to search for Android SDK libraries. androidSdk: AndroidSdk( addGradleDeps: true, androidExample: packageRoot, @@ -85,7 +88,9 @@ ), output: Output( dart: DartOutput( + // Required. Output path for generated bindings. path: packageRoot.resolve('lib/android_utils.g.dart'), + // Optional. Write bindings into a single file (instead of one file per class). structure: OutputStructure.singleFile, ), ), @@ -105,23 +110,12 @@ <!-- file://./example/in_app_java/lib/main.dart#show_toast --> ```dart - /// Display device model number and the number of times this was called - /// as Toast. void showToast() { - final toastCount = hashmap.getOrDefault( - "toastCount".toJString(), - 0.toJString(), - ); - final newToastCount = (int.parse(toastCount!.toDartString()) + 1).toJString(); - hashmap.put("toastCount".toJString(), newToastCount); - final emoji = - emojiCompat.hasEmojiGlyph(sunglassEmoji.toJString().as(CharSequence.type)) - ? sunglassEmoji - : ':cool:'; - final message = - '${newToastCount.toDartString()} - ${Build.MODEL!.toDartString()} $emoji'; + final activity = + androidActivity(PlatformDispatcher.instance.engineId!)?.as(Activity.type); + final message = 'This is a native toast shown from a Flutter app via JNI.'; AndroidUtils.showToast( - androidActivity(PlatformDispatcher.instance.engineId!)?.as(Activity.type), + activity, message.toJString().as(CharSequence.type), 0, );
diff --git a/pkgs/jnigen/doc/lifecycle.md b/pkgs/jnigen/doc/lifecycle.md index 8778f29..0e19fd7 100644 --- a/pkgs/jnigen/doc/lifecycle.md +++ b/pkgs/jnigen/doc/lifecycle.md
@@ -79,13 +79,13 @@ - When an original Java object is no longer needed, set `releaseOriginal` to `true` during conversion to Dart equivalents or casting. - <!-- no-source-file --> + <!-- file://./../example/api/lifecycle_snippet.dart#release_original --> ```dart final foo = Foo(); final String string = foo.someJString().toDartString(releaseOriginal: true); final JInteger jint = - foo.someJNumber().as(JInteger.type, releaseOriginal: true); - final int dartInt = castedAsInteger.intValue(releaseOriginal: true); + foo.someJNumber().as(JInteger.type, releaseOriginal: true); + final int dartInt = jint.toDartInt(releaseOriginal: true); foo.release(); // All references are removed. ``` @@ -103,13 +103,13 @@ One could create cycles between Dart and Java GC's when implementing interfaces. For example consider the following: -<!-- no-source-file --> +<!-- file://./../example/api/lifecycle_snippet.dart#cycle --> ```dart final foo = Foo(); foo.bar = Bar.implement($Bar( f: () { - return foo; - } + return foo; + }, )); ``` @@ -127,17 +127,17 @@ To prevent cycles, use [`WeakReference`](https://api.dart.dev/dart-core/WeakReference-class.html)s. -<!-- no-source-file --> +<!-- file://./../example/api/lifecycle_snippet.dart#cycle_breaker --> ```dart final weakFoo = WeakReference(foo); foo.bar = Bar.implement($Bar( f: () { final foo = weakFoo.target; if (foo == null) { - throw StateError(); + throw StateError('Foo was collected'); } return foo; - } + }, )); ``` @@ -156,7 +156,7 @@ > overcapturing, implement your logic in a separate function or create a class > that implements `$Bar`. > -> <!-- no-source-file --> +> <!-- file://./../example/api/lifecycle_snippet.dart#cycle_breaker_class --> > ```dart > final class BarImpl with $Bar { > final WeakReference<Foo> weakFoo; @@ -164,16 +164,16 @@ > BarImpl(this.weakFoo); > > @override -> void f() { +> Foo f() { > final foo = weakFoo.target; > if (foo == null) { -> throw StateError(); +> throw StateError('Foo was collected'); > } > return foo; > } > } > -> void main() { +> void cycleBreakerClassMain(Foo foo) { > final weakFoo = WeakReference(foo); > foo.bar = Bar.implement(BarImpl(weakFoo)); > // ...
diff --git a/pkgs/jnigen/example/api/lifecycle_snippet.dart b/pkgs/jnigen/example/api/lifecycle_snippet.dart index e173e27..18a2096 100644 --- a/pkgs/jnigen/example/api/lifecycle_snippet.dart +++ b/pkgs/jnigen/example/api/lifecycle_snippet.dart
@@ -4,13 +4,45 @@ // dart format width=74 -// ignore_for_file: avoid_print, unused_local_variable +// ignore_for_file: avoid_dynamic_calls, avoid_print +// ignore_for_file: non_constant_identifier_names, omit_local_variable_types +// ignore_for_file: unused_local_variable -import 'package:ffi/ffi.dart'; import 'package:jni/jni.dart'; dynamic JList; +class Foo { + Bar? bar; + + JString someJString() => throw UnimplementedError(); + + JObject someJNumber() => throw UnimplementedError(); + + void release() {} +} + +class Bar { + Bar._(); + + factory Bar.implement($Bar impl) => Bar._(); +} + +abstract base mixin class $Bar { + factory $Bar({required Foo Function() f}) = _BarImpl; + + Foo f(); +} + +final class _BarImpl with $Bar { + final Foo Function() _f; + + _BarImpl({required Foo Function() f}) : _f = f; + + @override + Foo f() => _f(); +} + void releaseManualExample() { // snippet-start#release_manual // Construct the object. @@ -47,3 +79,65 @@ print(jstrings.length); // prints 2. // snippet-end#java_collections } + +void releaseOriginalExample() { + // snippet-start#release_original + final foo = Foo(); + final String string = foo.someJString().toDartString(releaseOriginal: true); + final JInteger jint = + foo.someJNumber().as(JInteger.type, releaseOriginal: true); + final int dartInt = jint.toDartInt(releaseOriginal: true); + foo.release(); + // All references are removed. + // snippet-end#release_original +} + +void cycleExample() { + // snippet-start#cycle + final foo = Foo(); + foo.bar = Bar.implement($Bar( + f: () { + return foo; + }, + )); + // snippet-end#cycle +} + +void cycleBreakerExample() { + final foo = Foo(); + // snippet-start#cycle_breaker + final weakFoo = WeakReference(foo); + foo.bar = Bar.implement($Bar( + f: () { + final foo = weakFoo.target; + if (foo == null) { + throw StateError('Foo was collected'); + } + return foo; + }, + )); + // snippet-end#cycle_breaker +} + +// snippet-start#cycle_breaker_class +final class BarImpl with $Bar { + final WeakReference<Foo> weakFoo; + + BarImpl(this.weakFoo); + + @override + Foo f() { + final foo = weakFoo.target; + if (foo == null) { + throw StateError('Foo was collected'); + } + return foo; + } +} + +void cycleBreakerClassMain(Foo foo) { + final weakFoo = WeakReference(foo); + foo.bar = Bar.implement(BarImpl(weakFoo)); + // ... +} +// snippet-end#cycle_breaker_class
diff --git a/pkgs/jnigen/example/in_app_java/lib/main.dart b/pkgs/jnigen/example/in_app_java/lib/main.dart index 3c1e43c..82f8d12 100644 --- a/pkgs/jnigen/example/in_app_java/lib/main.dart +++ b/pkgs/jnigen/example/in_app_java/lib/main.dart
@@ -26,9 +26,21 @@ const sunglassEmoji = "😎"; // snippet-start#show_toast +void showToast() { + final activity = + androidActivity(PlatformDispatcher.instance.engineId!)?.as(Activity.type); + final message = 'This is a native toast shown from a Flutter app via JNI.'; + AndroidUtils.showToast( + activity, + message.toJString().as(CharSequence.type), + 0, + ); +} +// snippet-end#show_toast + /// Display device model number and the number of times this was called /// as Toast. -void showToast() { +void showToastFancy() { final toastCount = hashmap.getOrDefault( "toastCount".toJString(), 0.toJString(), @@ -47,7 +59,6 @@ 0, ); } -// snippet-end#show_toast void main() { EmojiCompat.init(context.as(Context.type)); @@ -82,7 +93,7 @@ children: [ ElevatedButton( child: const Text('Show Device Model'), - onPressed: () => showToast(), + onPressed: () => showToastFancy(), ), ], ),
diff --git a/pkgs/jnigen/example/in_app_java/tool/jnigen.dart b/pkgs/jnigen/example/in_app_java/tool/jnigen.dart index 35321b7..9d5fe67 100644 --- a/pkgs/jnigen/example/in_app_java/tool/jnigen.dart +++ b/pkgs/jnigen/example/in_app_java/tool/jnigen.dart
@@ -6,13 +6,16 @@ final packageRoot = Platform.script.resolve('../'); final generator = JniGenerator( input: Input( - sourcePath: [packageRoot.resolve('android/app/src/main/java')], + // Required. List of classes or packages for which bindings should be generated. classes: [ 'com.example.in_app_java', // Generate the entire package 'androidx.emoji2.text.EmojiCompat', // From gradle's compile classpath 'androidx.emoji2.text.DefaultEmojiCompatConfig', // From gradle's compile classpath 'android.os.Build', // from gradle's compile classpath ], + // Optional. List of directories that contain the source files for which to generate bindings. + sourcePath: [packageRoot.resolve('android/app/src/main/java')], + // Optional. Configuration to search for Android SDK libraries. androidSdk: AndroidSdk( addGradleDeps: true, androidExample: packageRoot, @@ -20,7 +23,9 @@ ), output: Output( dart: DartOutput( + // Required. Output path for generated bindings. path: packageRoot.resolve('lib/android_utils.g.dart'), + // Optional. Write bindings into a single file (instead of one file per class). structure: OutputStructure.singleFile, ), ),