Skip to main content

ctrlapp 0.1.4 copy "ctrlapp: ^0.1.4" to clipboard
ctrlapp: ^0.1.4 copied to clipboard

Remote kill-switch, feature flags, forced update, forms and inbox for Flutter, plus diagnostics, device-integrity checks, push permission and hosted login.

ctrlapp — remote control for your Flutter app #

Change how your live app behaves from the CtrlApp dashboard — without publishing a new version.

Turn off a broken feature, show a maintenance screen, force an update, change a button label, or run an experiment, and your users' apps pick it up automatically, usually within a minute.

What you can do #

You want to... You use
Switch off the whole app, or one broken feature, instantly the kill-switch (isBlocked, boolFlag)
Show a "please update" screen to old versions forced update (blockChanges)
Turn features on for 5% of users, then 100% feature flags (boolFlag)
Change numbers, texts, images or links remotely remote config (configValue, text, image)
Test two versions of something (A/B) experiments (treatment)
Collect feedback or sign-ups forms (submitForm) and an in-app inbox
Stop screenshots and screen recording of sensitive screens screen capture protection (a switch in the dashboard)

It keeps working offline: the last known decision is cached on the device, so a killed feature stays killed even if the user opens the app without internet.

Get started in 3 steps #

1. Create an app at ctrlapp.dev and copy its API key (it starts with ck_sdk_).

2. Add the package

flutter pub add ctrlapp

3. Start it when your app launches

import 'package:ctrlapp/ctrlapp.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await CtrlApp.init(apiKey: 'ck_sdk_YOUR_KEY');
  runApp(const MyApp());
}

That's it. From anywhere in your app you can now use CtrlApp.instance.

Everyday use #

final ctrl = CtrlApp.instance;

// Kill-switch: should the app (or a feature) be blocked right now?
if (ctrl.isBlocked) {
  showMaintenanceScreen(ctrl.blockReason());
}

// React when you flip the switch in the dashboard while the app is open.
ctrl.blockChanges.listen((block) {
  if (block.action == 'force_update') openStore(block.forceUpdateUrl);
});

// Feature flag
if (ctrl.boolFlag('new_checkout')) {
  showNewCheckout();
}

// Remote config, text and images (with safe defaults)
final maxItems = ctrl.configValue<int>('cart.max_items', defaultValue: 10);
final title = ctrl.text('home.title', fallback: 'Welcome');
final banner = ctrl.image('home.banner'); // null if not set

// Experiment: which version should this user see?
final variant = await ctrl.treatment('checkout_test', defaultValue: 'control');

// Track what people do
ctrl.track('purchase', props: {'plan': 'pro'});

Every call returns the default you pass in if the control doesn't exist, so your app never breaks because a key is missing.

Modals, banners and ads #

Send them from the dashboard; the package shows them for you. There is nothing to add to your app: they use your app's Theme, so they look like part of it.

Type What it looks like
Modal A card in the middle with picture, text and a button
Banner A slim card at the top that hides itself
Ad · Interstitial A full-screen ad with a countdown ring
Ad · Banner A banner ad along the bottom edge
Ad · Inline A "Sponsored" card you place in a list (see below)

Countdown ads. In the dashboard you can set "Must watch for (seconds)" on an ad. The close button stays locked while a timer counts down, and the back button does nothing. When it ends the person can close the ad and will not see it again. If they leave the app early, the ad is removed unfinished and shown again the next time they open the app. The dashboard counts every showing as a view, and the number of different devices as viewers.

// Inline ads: put this where you want a sponsored card. It is empty when there
// is no ad, so it is safe to leave in place.
ListView(children: [item1, item2, const CtrlAppInlineAd(), item3]);

Make it your own. Change the colors and text, or replace any design:

await CtrlApp.init(
  apiKey: 'ck_sdk_YOUR_KEY',
  messageUi: CtrlAppMessageUi(
    accentColor: Colors.deepOrange,
    labelsBuilder: () => CtrlAppMessageLabels(open: 'Ouvrir', close: 'Fermer'),
    // Replace a design completely. `controller` gives you the countdown.
    interstitialBuilder: (context, message, controller) => MyFullScreenAd(
      message: message,
      secondsLeft: controller.secondsLeft, // ValueListenable<int>
      onClose: controller.close,           // ignored until the countdown ends
      onTap: controller.click,             // opens the link, counts the tap
    ),
  ),
);

Use CtrlAppMessageUi(enabled: false) to draw nothing and handle CtrlApp.instance.messages yourself. Push and inbox messages are never drawn by this.

Send things to a group (tags) #

Tag your users from your app, then target a campaign or a feature flag at that tag in the dashboard:

await CtrlApp.instance.addTags(['bidder', 'vip']);
await CtrlApp.instance.removeTags(['new-user']);

Tags you set in the dashboard are kept. Tags are per device, and each device can have up to 50 of up to 64 characters.

Screen capture protection #

Turn it on per app in the dashboard (it is off by default). The package then applies it automatically:

  • Android: blocks screenshots and screen recording of your app (FLAG_SECURE). It never clears a flag your own code set.
  • iOS: hides your app's content in the app switcher and covers it while the screen is being recorded or mirrored. Apple provides no way to block a plain screenshot, so iOS cannot prevent those.

More in this package #

Everything is one package. Next to CtrlApp you can use these on their own:

Need Use
Crash and error reports (opt-in, sensitive data removed) CtrlAppDiagnostics, CtrlAppHandlers
Detect rooted / jailbroken devices and emulators CtrlAppIntegrity.detect()
Device model and OS version CtrlAppDeviceInfo.detect()
Ask for notification permission CtrlAppPush.requestPermission()
Re-check the kill-switch while the app stays open CtrlAppBlockWatch
Sign-in with Google and other providers package:ctrlapp/auth.dart
Test your app without a network MockCtrlAppTransport
final diagnostics = CtrlAppDiagnostics();
CtrlAppHandlers(diagnostics).install(); // collects Flutter and platform errors

final signals = await CtrlAppIntegrity.detect();
final permission = await CtrlAppPush.requestPermission();

The sign-in classes have short, generic names (Loading, Offline, ...), so they live in their own import: import 'package:ctrlapp/auth.dart';

Requirements #

  • Flutter 3.10 or newer, Dart 3
  • Android 5.0 (API 21) or newer, iOS 12 or newer

Help #

MIT license.

1
likes
130
points
448
downloads

Documentation

Documentation
API reference

Publisher

verified publisherctrlapp.dev

Weekly Downloads

Remote kill-switch, feature flags, forced update, forms and inbox for Flutter, plus diagnostics, device-integrity checks, push permission and hosted login.

Homepage

Topics

#feature-flags #kill-switch #remote-config #forced-update #screen-capture

License

MIT (license)

Dependencies

device_info_plus, flutter, http, package_info_plus, shared_preferences, url_launcher, video_player

More

Packages that depend on ctrlapp

Packages that implement ctrlapp