xray_device_actions 0.1.5
xray_device_actions: ^0.1.5 copied to clipboard
Lets the xRay desktop app drive a Flutter app — appearance, locale, text scale, animations and debug overlays — through the xray_inspector device inspector.
xray_device_actions #
Lets the xRay desktop app drive your Flutter app while you inspect it: appearance, locale, text scale, animation speed, bold text, high contrast, layout bounds and the performance overlay. Deep links, simulated push notifications and feature-flag overrides are registered on the same XRayDeviceInspector from xray_inspector.
For full documentation and guides, see the xRay docs.
Setup #
final deviceInspector = XRayDeviceInspector(
appVersion: () => '3.4.1 (412)',
);
final server = XRayInspectorServer(
config: XRayInspectorServerConfig(inspectors: [deviceInspector /* , … */]),
);
// Debug builds only, see the xray_inspector README.
if (!kReleaseMode) await server.start();
runApp(XRayDeviceOverrides(
inspector: deviceInspector,
supportedLocales: AppLocalizations.supportedLocales,
child: const MyApp(),
));
XRayDeviceOverrides must sit above MaterialApp. Appearance, text scale, bold text, high contrast and "animations off" reach your app through MediaQuery. For the locale override, pass the chosen locale to your app:
MaterialApp(
locale: XRayDeviceOverrides.localeOf(context), // null keeps your own logic
supportedLocales: AppLocalizations.supportedLocales,
// …
)
- Appearance overrides the platform brightness, so it works when your app follows the system theme (
ThemeMode.system). - Show layout bounds is Flutter's
debugPaintSizeEnabledand only paints in debug builds. - Quick settings live in memory: restarting the app brings back its own values.
- Screenshots: the wrapper also lets xRay capture the app's screen (the camera button in the device drawer, or
take_screenshotover MCP). It captures what Flutter draws below the wrapper, including dialogs, but not the status bar, keyboard or system dialogs, and only while the app is on screen. The image is not redacted.
Deep links #
deviceInspector.deepLinkHandler = XRayDeepLinkHandler(
name: 'GoRouter',
onOpen: (uri) {
router.go(uri.hasScheme ? uri.path : uri.toString());
return XRayDeepLinkResult.handled(
destination: router.routerDelegate.currentConfiguration.uri.toString(),
);
},
routes: () => const [
XRayRoute(path: '/', name: 'HomeScreen'),
XRayRoute(path: '/product/:id', name: 'ProductScreen'),
],
);
Return XRayDeepLinkResult.notHandled('reason') for links your app does not know. A handler that throws is reported as not handled.
Push notifications #
Push notifications sent from the desktop app are simulated: nothing goes through APNs or FCM. The inspector calls your handlers in-process with the title, body and data payload, so you test what your app does with a message, not the delivery itself.
deviceInspector.pushHandler = XRayPushHandler(
onForeground: (message) => showInAppBanner(message.title, message.body),
onBackground: (message) => syncInBackground(message.data),
onOpened: (message) => router.go(message.data['deeplink'] as String),
);
Feature flags #
Implement XRayFeatureFlagAdapter for each flag source (your remote config, a vendor SDK, local debug flags). Store overrides on the device, so they survive restarts until they are reset from the desktop app:
class RemoteConfigAdapter implements XRayFeatureFlagAdapter {
@override
String get name => 'Remote Config';
@override
Future<List<XRayFeatureFlag>> readFlags() async => [
XRayFeatureFlag(
key: 'new_checkout_flow',
type: XRayFeatureFlagType.boolean,
remoteValue: remoteConfig.getBool('new_checkout_flow'),
overrideValue: _override('new_checkout_flow'),
isOverridden: prefs.containsKey('override.new_checkout_flow'),
),
];
@override
Future<void> setOverride(String key, Object? value) =>
prefs.setString('override.$key', jsonEncode(value));
@override
Future<void> clearOverride(String key) => prefs.remove('override.$key');
Object? _override(String key) {
final raw = prefs.getString('override.$key');
return raw == null ? null : jsonDecode(raw);
}
}
Your app reads the override first and falls back to the remote value.
Custom pages #
Add pages of your own controls to the drawer: debug switches, an environment picker, a button that clears a cache. Each page becomes a tab after Flags, and the desktop app draws the controls, so you write no UI. Every control takes a getter for its current value and a callback for changes:
final debugPage = XRayDevicePage(
id: 'debug',
title: 'Debug',
icon: XRayPageIcon.bug,
onReset: config.resetDebugOptions,
controls: [
const XRaySectionHeader('Backend'),
XRaySelect(
id: 'environment',
label: 'Environment',
options: const [
XRayOption('dev', 'Development'),
XRayOption('prd', 'Production'),
],
value: () => config.environment,
overridden: () => config.environment != 'prd',
onChanged: config.switchEnvironment,
),
XRayToggle(
id: 'mock-api',
label: 'Use mock API',
value: () => config.mockApi,
overridden: () => config.mockApi,
onChanged: (value) => config.mockApi = value,
),
XRayInfo(label: 'User id', value: () => session.userId ?? '—'),
XRayButton(
id: 'clear-cache',
label: 'Clear image cache',
confirm: 'Remove every cached image?',
onPressed: () async => '${await cache.clear()} files removed',
),
],
);
final deviceInspector = XRayDeviceInspector(pages: [debugPage]);
| Control | Value | Sent to your callback |
|---|---|---|
XRayToggle |
bool |
on every flip |
XRayTextInput |
String (hint, isMultiline) |
on Enter or when the field loses focus, only when it changed |
XRaySlider |
double from min to max (divisions, unit) |
when the drag ends |
XRaySegmentedControl |
an option value, 2 to 5 options | on select |
XRaySelect |
an option value, any number of options | on select |
XRayButton |
none (confirm, isDestructive) |
on press; return a message to show it as a toast |
XRayInfo |
String, read-only |
never |
XRaySectionHeader |
none; groups the controls below it | never |
- Overridden. Pass
overriddento say when a value differs from your app's own. The control then shows Overridden, and the drawer's footer counts it with the quick settings and flags. - Reset all calls each page's
onReset, which puts every value of that page back. A page withoutonResetis left alone. - Values that change in the app. When your app changes one of these values itself, call
debugPage.notifyChanged(). A connected desktop app reads the page again. Calls within 100 ms count as one. - Refusing a value. The inspector checks types, slider ranges and option values before your callback runs. To refuse a value with a message of your own, throw
XRayDeviceActionException.invalid('Must start with https://'). Anything a callback throws is shown next to the control. - Ids. Page ids are unique per inspector and control ids are unique per page. Use letters, digits,
_and-. Getters and callbacks may be async. Store the values yourself if they should survive a restart.
Disclaimer: This is a commercial package. To use this package, you need to have either a commercial xRay license or a free xRay Community License. The xRay desktop apps for macOS, Windows and Linux are the authorized UI clients for this library. For more details, please check the LICENSE file.