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
Native iOS Control Center with expense, note and focus controls Native iOS Lock Screen UI with Add Expense and New Note controls Native Android Quick Settings with Add Expense and Focus Session

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.

Native Android recording: toggle Focus Session twice, then open Add Expense

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 onAction after 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 onAction succeeds. A crash after the handler's side effect but before acknowledgement can redeliver the event; use action.eventId as 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.

Platform References

Libraries

system_controls
Native iOS controls and Android Quick Settings tiles.