system_controls 0.1.0
system_controls: ^0.1.0 copied to clipboard
Native iOS Control Center controls and Android Quick Settings tiles for Flutter, with shared state, durable actions, and native code generation.
system_controls #
Native system controls for Flutter: iOS Control Center, Lock Screen and Action button controls, plus Android Quick Settings tiles. A build-time YAML definition generates the Swift controls, App Intents, Kotlin services and Dart identifiers.
Native System UI #
Actual iOS 18.0 Simulator and Android 15 emulator captures, not Flutter recreations.
| iOS Control Center | iOS Lock Screen controls | Android Quick Settings |
|---|---|---|
![]() |
![]() |
![]() |
The Lock Screen UI is shown through the simulator's unlocked system cover sheet. These captures demonstrate placement and actions, not passcode or Face ID checks.
Watch the interaction: toggle Focus Session, then open the expense form.
MP4 recording · iOS control gallery · Lock Screen customization · Android add-tile prompt · Delivered actions
See capture notes for provenance, reproduction and release checks.
| Capability | iOS | Android |
|---|---|---|
| System surface | Controls, iOS 18+ | Quick Settings, API 24+ |
| Button | Opens the app and delivers an action | Opens the app and delivers an action |
| Toggle | Saves state in the App Group from an App Intent | Saves state directly in the TileService |
| Add prompt | User adds controls from the system gallery | Native placement prompt on API 33+ |
| Refresh | WidgetKit ControlCenter | TileService requestListeningState |
The new system UI here is Apple's iOS 18 Controls. Android Quick Settings tiles are the established Android counterpart, not a newly introduced Android feature.
Install #
Add the package to your app:
dependencies:
system_controls: ^0.1.0
Run flutter pub get, then create system_controls.yaml in your app root:
app_group: group.com.example.myapp
ios_bundle_identifier: com.example.myapp
android_package: com.example.myapp
controls:
- id: quick_note
title: New Note
description: Capture a thought
kind: button
ios_symbol: square.and.pencil
android_icon: '@drawable/ic_note'
- id: focus_session
title: Focus Session
kind: toggle
ios_symbol: timer
android_icon: '@drawable/ic_focus'
initial_value: false
requires_unlock: true
Supply monochrome Android drawable icons at those resource names. SF Symbol names must exist on the minimum iOS version you support. Up to 10 controls are supported. IDs must be stable, unique lower_snake_case identifiers.
dart run system_controls:generate --install-ios
This writes lib/system_controls.g.dart, ios/SystemControls/ and generated Kotlin
services. It updates the Android manifest using an XML parser. On macOS,
--install-ios uses Ruby's xcodeproj gem (normally installed with CocoaPods) to
add and embed a SystemControlsExtension target. Without that flag it generates
the files but does not connect the iOS target. Reruns update owned services and
targets without duplicating them. Handwritten destination files are not replaced.
The installer expects a standard ios/Runner.xcodeproj and Runner target.
Custom host targets require manual Xcode integration. Generated files should be
committed; rerun after changing YAML. Do not edit generated Swift or Kotlin files.
For custom native behavior, maintain your own native intents/services instead of
regenerating those files.
Dart API #
import 'package:system_controls/system_controls.dart';
import 'system_controls.g.dart';
final controls = SystemControls.instance;
Future<void> connectControls() async {
final availability = await controls.availability;
if (!availability.supported) return;
await controls.initialize(
appGroup: AppSystemControls.appGroup,
controls: AppSystemControls.controls,
onAction: (action) async {
switch (action.controlId) {
case 'quick_note':
// Navigate using your app's router once it is ready.
break;
case 'focus_session':
// Reconcile the app with the latest saved state.
final enabled = await controls.getValue('focus_session');
print('Focus session: $enabled');
break;
}
},
onError: (error, stack) {
// Log the failure; controls.refresh() retries unacknowledged actions.
},
);
}
await controls.setValue('focus_session', true);
final enabled = await controls.getValue('focus_session');
await controls.reload();
final result = await controls.requestAdd('quick_note');
Initialize after WidgetsFlutterBinding.ensureInitialized() and after your router
can accept navigation. Only one client per Flutter engine should be initialized.
Dispose the client if its owner is removed. Initial values seed missing state and
never reset a previously saved toggle. setValue refreshes the system UI without
generating a user action.
requestAdd returns added, alreadyAdded, declined or unsupported. Android
errors such as a missing foreground activity throw PlatformException; denial
is not treated as an error. iOS returns unsupported because it has no public
programmatic placement prompt. Users choose placement in Control Center, Lock
Screen customization or Action Button settings on supported hardware.
Native Setup #
iOS: Xcode 16+ is required to compile Controls. The plugin can be linked into an older iOS host, but Controls require iOS 18+. The generated extension targets iOS 18. Enable the same App Group and development team for Runner and the extension in Xcode. The installer adds entitlements locally; it does not create Apple Developer portal identifiers, capabilities or provisioning profiles.
ControlIntents.swift and SystemControlsStore.swift belong to both Runner and
the extension. ControlsWidgetBundle.swift belongs only to the extension. This
dual intent membership is necessary for OpenIntent to launch the app. The
extension has no Flutter engine or Flutter dependency. Swift Package Manager and
CocoaPods plugin layouts are included.
Android: minSdk 24, compileSdk 36 and Java 17. The generated services use
BIND_QUICK_SETTINGS_TILE, ACTIVE_TILE and, for toggles, TOGGLEABLE_TILE.
Services run in the default app process. Do not add android:process. Android
14+ launches the host via the required PendingIntent overload. Users can add
tiles manually on API 24–32; the native placement prompt requires API 33+.
Execution Contract #
This version deliberately has an explicit execution model:
- Buttons open the app. Dart handles routing through
onActionafter startup. - Toggles update native persisted state immediately without opening the app.
- A toggle represents your app's state. It does not toggle Apple's Focus, Android DND, a VPN, a light or another remote service automatically.
- Dart callbacks are delivered while the app engine is running, including on launch/resume. No background Dart isolate is created. Do not rely on Dart callbacks to perform an immediate background network or hardware operation.
- Native state and toggle events are committed together. iOS uses an atomically written App Group file guarded by a cross-process lock. Android uses a single preferences document guarded by a process-wide lock.
- Events are acknowledged only after
onActionsucceeds. A crash after the handler's side effect but before acknowledgement can redeliver the event; useaction.eventIdas an idempotency key for persistent side effects. - A failed handler stops the queue so later actions do not overtake it. Retry
with
refresh()or the next resume. Unknown/removed IDs should be handled gracefully by the app; they are not silently discarded. - Pending queues are limited to 1024 events. At capacity, new native actions fail without silently dropping events or committing a toggle change.
- Reloads are requests to the OS, not a guarantee of immediate rendering.
- App Group data is local to the device and is not a place to store credentials.
For workflows such as VPN or smart-home controls, execute the actual operation in a purpose-built native intent/service, then persist its confirmed state. Generic persisted toggles alone are not a substitute for those integrations.
Example And Verification #
The example includes expense/note entry, a focus-session state toggle, saved activity and Android tile placement buttons.
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
dart run system_controls:generate --install-ios
flutter test
flutter run
Native storage checks:
swiftc ios/system_controls/Sources/system_controls/SystemControlsStore.swift \
tool/native_store_test.swift -o /tmp/system-controls-store-tests
/tmp/system-controls-store-tests
cd example/android
./gradlew :system_controls:testDebugUnitTest
See verification.md for platform checks and device steps. Validation covers simulators/emulators and automated tests. Physical-device signing, Lock Screen authentication, Action Button hardware and OEM-specific Quick Settings behavior have not yet been verified.



