gallery: library-change Stream + native observer parity

Add UxGallery.libraryChanges — a Stream<void> that emits whenever the
underlying photo library reports a change, so picker UIs can drop
their cached asset lists and reload reactively.

- iOS: GalleryPlugin conforms to PHPhotoLibraryChangeObserver +
  FlutterStreamHandler; on photoLibraryDidChange the fetchCache is
  cleared and a void event is pushed over ux/gallery/changes. The
  observer is registered lazily on the first granted/limited
  authorization so plugin init doesn't trigger iOS's permission
  evaluation at app launch.
- macOS: near-verbatim port of the iOS shape (same Photos.framework,
  same fetchCache staleness, same fix).
- Android: registers a MediaStore ContentObserver on
  Files.getContentUri(VOLUME_EXTERNAL) with notifyForDescendants =
  true; observer lifecycle tracks the Dart subscription (registered
  on onListen, unregistered on onCancel / plugin detach). No native
  cache to invalidate today, but the pipeline is wired for the
  upcoming READ_MEDIA_VISUAL_USER_SELECTED limited-access work.
- iOS presentLimitedLibraryPicker switched to the iOS 15+ completion-
  handler variant so the Dart await resolves on dismissal, not on
  presentation. The actual reload is now driven by the change
  observer (which fires after iOS commits the new subset),
  side-stepping the completion-vs-commit race that produced an
  off-by-one in the picker on consecutive MANAGE taps.
- FakeUxGalleryBackend exposes emitLibraryChange() so tests can
  drive the reactive-reload wiring without going through the real
  method channel.
This commit is contained in:
agra
2026-05-11 09:52:17 +03:00
parent 3eba30358c
commit 77cda5a17e
5 changed files with 226 additions and 9 deletions

View File

@@ -91,6 +91,13 @@ abstract class UxGalleryBackend {
});
Future<UxAssetThumbnail> thumbnail(String assetId, {required int sizePx});
Future<io.File> resolveFile(String assetId);
/// Emits whenever the underlying photo library reports a change —
/// limited-subset edits via `presentLimitedLibraryPicker`, external
/// edits from Photos.app, or permission flips from Settings.
/// Subscribers should re-fetch any cached album / asset listings on
/// each event.
Stream<void> get libraryChanges;
}
/// Static facade for the system photo library. All state lives in the
@@ -150,6 +157,11 @@ class UxGallery {
/// app cache (since `dart:io` can't read content URIs directly).
static Future<io.File> resolveFile(String assetId) =>
backend.resolveFile(assetId);
/// Emits whenever the system photo library changes — limited-subset
/// edits, external edits, permission changes. Picker UIs subscribe
/// to reload their cached asset list reactively.
static Stream<void> get libraryChanges => backend.libraryChanges;
}
/// Default [UxGalleryBackend] — dispatches to native code via the
@@ -157,6 +169,14 @@ class UxGallery {
/// after swapping to a fake.
class MethodChannelGalleryBackend implements UxGalleryBackend {
static const _channel = MethodChannel('ux/gallery');
static const _changesChannel = EventChannel('ux/gallery/changes');
/// Lazy broadcast view of `ux/gallery/changes`. The native side only
/// holds a single sink; a broadcast Stream lets multiple Dart
/// listeners (e.g. picker + selected-strip) subscribe independently.
@override
late final Stream<void> libraryChanges =
_changesChannel.receiveBroadcastStream().map((_) {});
static String _kindArg(UxAssetKind? k) => switch (k) {
null => 'any',

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'dart:typed_data';
@@ -45,6 +46,17 @@ class FakeUxGalleryBackend implements UxGalleryBackend {
final UxAssetThumbnail Function(String assetId, int sizePx) _thumbnailFor;
final io.File Function(String assetId) _fileFor;
final StreamController<void> _libraryChanges =
StreamController<void>.broadcast();
/// Tests call this to simulate the platform `photoLibraryDidChange`
/// event — drives any reactive reload wiring without going through
/// the real method channel.
void emitLibraryChange() => _libraryChanges.add(null);
@override
Stream<void> get libraryChanges => _libraryChanges.stream;
/// Optional hook fired on `requestPermission`. Default updates
/// `permissionState` to [UxGalleryPermission.granted].
final UxGalleryPermission Function()? onRequestPermission;