8bed5435ad8828ba495dbf254753143226dedb14
Two changes that target the macOS "camera not found" leak after a
few open/close cycles. macOS's `AVCaptureDevice.DiscoverySession`
excludes devices that are still claimed by another session — even
our own zombie session that hasn't fully released its grip on the
hardware. So if dispose leaves the session in a partially torn-down
state, the next `availableCameras` returns empty.
CameraInstance.dispose now:
- Cancels the recorder (was already there) so the audio
data output's retain on the recorder drops.
- Stops the session.
- **Nils sample-buffer delegates** on the video + audio data
outputs before removing them. `setSampleBufferDelegate` holds a
strong reference to the delegate; the macOS reference to our
`SampleFanout` was transitively keeping the session alive.
- Removes inputs + outputs inside a single
`session.configure { … }` block (begin/commitConfiguration) so
AVFoundation sees the teardown as one atomic transition rather
than a sequence of partial states. Apple's docs are explicit on
this; we weren't following.
- Clears the strong references to the instance vars.
Plugin diagnostics:
- When availableCameras returns empty, native now emits an event
`{handle: -1, event: "diagnostic", message: …}` carrying the
current `devicesInUse`, `audioInUse` and `instances` keys.
Per-handle diagnostics already flow through a controller's
`_onEvent`; plugin-level ones (handle == -1) had no path to the
log_server jsonl.
- `MethodChannelUxCameraBackend` now subscribes to its raw event
stream once and pipes any handle=-1 diagnostic through
`ux.Log.tag('camera').i('plugin: …')`. The subscription kicks in
when the broadcast stream is first accessed (still lazy —
matches the prior behavior).
If the macOS "camera not found" reproduces, the jsonl will show
which side leaked: a non-empty `devicesInUse` says our claim
tracking is stale; an empty one says AVFoundation itself is
holding the hardware.
ux
A Flutter toolkit for building fluid, native-feeling UIs.
UxKeyboard
Frame-accurate keyboard height tracking for iOS and Android, with interactive dismiss.
Flutter's built-in MediaQuery.viewInsets.bottom lags behind the actual keyboard position
and doesn't support interactive dismiss. UxKeyboard reads the keyboard height directly
from the native layer via FFI — zero channel latency, every frame.
Features
- Real-time height — reads the keyboard's actual position each frame via FFI (iOS) / JNI (Android)
- Native animation curves — sampled from
CADisplayLink(iOS) andWindowInsetsAnimation(Android), with adaptive learning that refines the curve from observations - Interactive dismiss — swipe the keyboard down like iMessage/Telegram, with snap-back or dismiss
- Scroll freeze —
isTrackingflag lets you freeze scrolling during interactive dismiss
Quick start
final keyboard = UxKeyboard.instance;
// Enable swipe-to-dismiss. trackingInset is the height of your input bar.
keyboard.enableInteractiveDismiss(trackingInset: 56);
Use ListenableBuilder to rebuild when the keyboard height changes:
Scaffold(
resizeToAvoidBottomInset: false, // we handle it ourselves
body: ListenableBuilder(
listenable: keyboard,
builder: (context, _) {
final keyboardHeight = keyboard.height;
final safeBottom = MediaQuery.viewPaddingOf(context).bottom;
final bottom = max(keyboardHeight, safeBottom);
return Column(
children: [
Expanded(
child: ListView.builder(
reverse: true,
// Freeze scrolling during interactive dismiss
physics: keyboard.isTracking
? NeverScrollableScrollPhysics()
: null,
// ...
),
),
Container(
padding: EdgeInsets.only(bottom: 8 + bottom),
// your input bar
),
],
);
},
),
);
API
| Member | Description |
|---|---|
UxKeyboard.instance |
Singleton instance |
.height |
Current keyboard height in logical pixels |
.systemHeight |
Last system-reported keyboard height |
.isOpen |
Whether the keyboard is visible |
.isTracking |
Whether a dismiss pan gesture is active |
.enableInteractiveDismiss({trackingInset}) |
Enable swipe-to-dismiss |
.disableInteractiveDismiss() |
Disable swipe-to-dismiss |
addListener / removeListener |
Standard ChangeNotifier API |
Key points
- Set
resizeToAvoidBottomInset: falseon yourScaffold— otherwise Flutter's built-in resize fights withUxKeyboard - Use
MediaQuery.viewPaddingOf(context).bottomfor the safe area (notpaddingOf, which is consumed byScaffold) - Use
max(keyboardHeight, safeBottom)for bottom padding — the keyboard height includes the safe area when open, andsafeBottomcovers the home indicator when closed
Other utilities
- BendBox — a flexible layout widget
- Bezier — bezier curve utilities
Languages
Dart
40.2%
Swift
29.1%
Kotlin
18.4%
Java
5.2%
C++
3.1%
Other
4%