adaptable_remote_config 0.5.1
adaptable_remote_config: ^0.5.1 copied to clipboard
Firebase, HTTP, or custom remote config for Flutter as plain key/value pairs, with typed reads, a cached last-good snapshot, and optional/required app-update gates.
adaptable_remote_config #
Tell old versions of your app to update, without releasing a new app.
- Force update: old versions see a full-screen wall. They must update to continue.
- Optional update: old versions see a small dialog. They can tap Later or Skip.
You change a few values on a server. The app reads them and decides what to show. Works on Android and iOS.
How it works (60 seconds) #
1. YOU change a value "minimum version is now 2.0.0"
│ (in Firebase, in a JSON file, or in your own code)
▼
2. The app reads it when it starts, and again when it comes back
│ to the screen after about an hour
▼
3. The app compares installed version vs the version you set
│
▼
4. It decides fine → nothing happens
a bit old → optional dialog
too old → force-update wall
Three things are involved:
| Piece | What it is |
|---|---|
| Source | Where your values live. You pick one (see below). |
| Controller | Reads the values and decides. You create one. |
| UpdateGate | A widget around your app. It shows the wall or dialog. |
If the app cannot reach the source (no internet), it uses the last values it saved. If there is nothing saved, it lets the user in. It never blocks users by mistake.
Not sure what a value will do? See the decision table: it shows, for every combination of values, what the user will see.
Step 0: Pick your method #
| Method | Choose it if... | Needs |
|---|---|---|
| 1. Firebase | You already use Firebase, or you want the easiest way to edit values. | A Firebase project |
| 2. A JSON file | You do not use Firebase. You can put a small file on any website. | A place to host a file |
| 3. Your own code | You already have a backend API with app settings. | An API that returns settings |
Not sure? Choose Firebase.
The fast way: let the setup tool do it #
Run these two commands in your Flutter app folder:
flutter pub add adaptable_remote_config
dart run adaptable_remote_config
The tool asks a few questions. Use the arrow keys, space and Enter. Then it:
- Writes
lib/app_update.dart(the controller). - Wraps your
MaterialAppwithUpdateGate. - For Firebase: starts Firebase in
main(), and addsfirebase_coreif missing. - For Firebase: creates the values in your Firebase project (it shows them first and asks). If you give the app a prefix, it puts the values in their own folder in the Firebase console (see Two apps in one Firebase project below).
It never changes a value that already exists. If it cannot edit a file safely, it leaves the file alone and shows you the few lines to add yourself.
No questions at all:
dart run adaptable_remote_config --yes
More options:
| Option | What it does |
|---|---|
--source firebase (or http, custom) |
Choose the method without being asked |
--mode both (or force, optional) |
Choose the update types without being asked |
--url https://... |
The JSON file address (for http) |
--publish |
Create the Firebase values without asking |
--no-publish |
Do not touch Firebase, only print the values to create |
--prefix customer |
Several apps in one Firebase project: name the keys customer_android_version... (see below) |
--group "Customer app" |
Put the keys in this Firebase console folder (default: the prefix) |
--no-group |
Do not use a console folder |
--project my-project-id |
Choose the Firebase project |
--dir path / --out path |
Choose the app folder / the file to write |
With --yes, nothing is written to your Firebase project unless you add --publish.
Prefer to do it yourself? Follow your method below.
Method 1: Firebase #
What you need #
- A Flutter app connected to Firebase (
flutterfire configuredone once). - The packages
firebase_coreandadaptable_remote_config.
How it works #
Firebase Remote Config stores a list of named values (like android_version).
You edit them in the Firebase website. The app downloads them. No app release needed.
Steps #
1. Add the packages
flutter pub add adaptable_remote_config firebase_core
2. Connect your app to Firebase (once)
dart pub global activate flutterfire_cli
flutterfire configure
This creates lib/firebase_options.dart.
3. Start Firebase in main()
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const MyApp());
}
4. Create lib/app_update.dart
import 'package:adaptable_remote_config/adaptable_remote_config.dart';
class AppUpdate {
AppUpdate._();
static final RemoteConfigController controller = RemoteConfigController(
appId: 'my_app', // any name; it labels the saved copy on the phone
provider: FirebaseConfigProvider(
// The values used when Firebase has nothing yet. These change nothing.
defaults: const {
'android_version': '1.0.0+1',
'ios_version': '1.0.0+1',
'force_update': false,
'android_latest_version': '1.0.0+1',
'ios_latest_version': '1.0.0+1',
'android_store_link': '',
'ios_store_link': '',
},
),
subscribeToChanges: true,
);
}
5. Wrap your app with UpdateGate
MaterialApp(
builder: (context, child) => UpdateGate(
controller: AppUpdate.controller,
child: child!,
),
// ...
)
(Use the same builder: on MaterialApp.router.) If you already have a builder:,
call UpdateGate(...) inside it, around the widget it returns.
6. Create the values in Firebase
Firebase console → Remote Config → Add parameter. Create these:
| Name | Type | Start with | Meaning |
|---|---|---|---|
android_version |
String | 1.0.0+1 |
Oldest Android version allowed |
ios_version |
String | 1.0.0+1 |
Oldest iOS version allowed |
force_update |
Boolean | false |
true turns on the force-update wall |
android_latest_version |
String | 1.0.0+1 |
Newest Android version (for the optional dialog) |
ios_latest_version |
String | 1.0.0+1 |
Newest iOS version |
android_store_link |
String | (empty) | Play Store link. Empty = the app finds it itself |
ios_store_link |
String | (empty) | App Store link. Required for iOS |
Press Publish changes. (The setup tool can create all of these for you.)
7. Run the app. Nothing should change yet. That is correct.
Test it #
(What will each value do? See the decision table.)
| I want to... | Set this in Firebase, then Publish changes |
|---|---|
| See the optional dialog | android_latest_version = a number higher than your installed version, e.g. 9.0.0+1 |
| See the force wall | android_version = 9.0.0+1 and force_update = true |
| Go back to normal | Put the old values back (1.0.0+1 and false) and publish |
Then close the app fully and open it again. Users get the change the next time they start the app, usually within an hour (Firebase keeps a copy for up to an hour).
Careful: Firebase has no test mode. A force-update value reaches all your users. Test with the optional dialog first.
Two apps in one Firebase project? Give each app a prefix #
Many teams use one Firebase project for several apps (for example a customer app
and a driver app). They would all read the same names, such as android_version, and
change each other. A prefix fixes this: each app gets its own names, and its own
folder in the Firebase console.
Firebase console → Remote Config
├── 📁 customer ← customer app
│ customer_android_version
│ customer_ios_version
│ customer_force_update
│ ...
└── 📁 driver ← driver app
driver_android_version
driver_ios_version
driver_force_update
...
The easy way. In each app, run the setup tool with its own prefix:
dart run adaptable_remote_config --prefix customer # in the customer app
dart run adaptable_remote_config --prefix driver # in the driver app
It creates the keys with that prefix inside a folder with the same name. (You can also answer the questions: choose With a prefix, then type the prefix and the folder.) Keys that already exist are never changed.
By hand. Do this for each app:
-
Choose a short prefix: lowercase letters, digits and
_only (customer,driver). -
In
lib/app_update.dart, tell the controller the prefix, and use the prefixed names indefaults:static final RemoteConfigController controller = RemoteConfigController( appId: 'customer', keys: const UpdateKeys.prefixed('customer'), provider: FirebaseConfigProvider( defaults: const { 'customer_android_version': '1.0.0+1', 'customer_ios_version': '1.0.0+1', 'customer_force_update': false, 'customer_android_latest_version': '1.0.0+1', 'customer_ios_latest_version': '1.0.0+1', 'customer_android_store_link': '', 'customer_ios_store_link': '', }, ), subscribeToChanges: true, ); -
In the Firebase console, open Remote Config, choose Add new → Parameter group, and name it
customer. -
Add the seven keys above to that group (same types as in step 6), then Publish changes.
Each app now only reads its own keys, so changing the driver app never touches the customer app.
Method 2: A JSON file #
What you need #
- A small text file on any website (your server, GitHub Pages, Firebase Hosting...).
- The address must start with
https://.
How it works #
The app downloads your file when it starts. You edit the file to change the values.
Steps #
1. Add the package
flutter pub add adaptable_remote_config
2. Create the file (for example app_config.json) and upload it:
{
"android": {
"minimum_version": "1.0.0+1",
"latest_version": "1.0.0+1",
"force_update": false,
"store_url": "https://play.google.com/store/apps/details?id=com.example.app"
},
"ios": {
"minimum_version": "1.0.0+1",
"latest_version": "1.0.0+1",
"force_update": false,
"store_url": "https://apps.apple.com/app/id1234567890"
}
}
3. Create lib/app_update.dart
import 'package:adaptable_remote_config/adaptable_remote_config.dart';
class AppUpdate {
AppUpdate._();
static final RemoteConfigController controller = RemoteConfigController(
appId: 'my_app',
provider: HttpConfigProvider(
url: Uri.parse('https://example.com/app_config.json'),
),
keys: const UpdateKeys.nested(), // tells the app your file uses "android": {...}
);
}
If your file is inside another object, for example {"data": {"app": {...}}},
add root: 'data.app' to HttpConfigProvider.
4. Wrap your app with UpdateGate (same as Firebase, step 5):
MaterialApp(
builder: (context, child) => UpdateGate(
controller: AppUpdate.controller,
child: child!,
),
)
5. Run the app. Nothing should change yet.
Test it #
(What will each value do? See the decision table.)
Edit the file on your server:
- Optional dialog: set
latest_versionto9.0.0+1. - Force wall: set
minimum_versionto9.0.0+1andforce_updatetotrue. - Back to normal: put
1.0.0+1andfalseagain.
Close the app fully and open it again. Make sure your server does not cache the file for a long time.
Method 3: Your own code #
What you need #
- Any code that returns your settings as a map: your own API, a database, anything.
How it works #
You give the controller a function. The app calls it when it starts. The function returns the values. You decide where they come from.
Steps #
1. Add the package
flutter pub add adaptable_remote_config
2. Create lib/app_update.dart
import 'package:adaptable_remote_config/adaptable_remote_config.dart';
class AppUpdate {
AppUpdate._();
static final RemoteConfigController controller = RemoteConfigController(
appId: 'my_app',
provider: CustomConfigProvider(() async {
final settings = await myApi.getAppSettings(); // your own call
return {
'android_version': settings.minAndroid, // e.g. '2.0.0+1'
'ios_version': settings.minIos,
'force_update': settings.forceUpdate, // true or false
'android_latest_version': settings.latestAndroid,
'ios_latest_version': settings.latestIos,
'ios_store_link': settings.iosStoreUrl,
};
}),
);
}
The function must return a map (or throw an error if it fails; the app then uses the saved values). These names are the default names. You can use your own names, see Use your own key names below.
3. Wrap your app with UpdateGate (same as the other methods).
4. Run the app. Nothing should change yet.
Test it #
(What will each value do? See the decision table.)
Make your API return a higher android_version with force_update: true, or a higher
android_latest_version. Close the app and open it again.
Good to know #
What will the user see? (the decision table) #
The app asks three questions, in this order. It stops at the first "yes". Only one thing is ever shown.
1. Is force_update ON, and is the installed app OLDER than the minimum version?
yes → WALL (full screen, cannot be closed)
2. Is the installed app OLDER than the latest version?
yes → DIALOG (small box, the user can say Later)
3. Otherwise
→ NOTHING
"Installed app" is the version in your pubspec.yaml, for example 1.2.0+4.
"Minimum" is android_version / ios_version. "Latest" is android_latest_version /
ios_latest_version.
Table 1: the normal cases (installed app is 1.2.0+4) #
| # | force_update |
Minimum | Latest | The user sees | Why |
|---|---|---|---|---|---|
| 1 | off | (empty) | (empty) | Nothing | Nothing is set |
| 2 | off | 2.0.0 |
(empty) | Nothing | The minimum only counts when force_update is on |
| 3 | ON | 2.0.0 |
(empty) | WALL | Installed is older than the minimum |
| 4 | ON | 1.2.0+4 or lower |
(empty) | Nothing | Installed meets the minimum |
| 5 | ON | (empty) | (empty) | Nothing | No minimum, so nothing to enforce |
| 6 | off | (empty) | 2.0.0 |
DIALOG | Installed is older than the latest |
| 7 | off | (empty) | 1.2.0+4 or lower |
Nothing | Installed is up to date |
| 8 | ON | 2.0.0 |
3.0.0 |
WALL | The wall wins over the dialog |
| 9 | ON | 1.0.0 |
2.0.0 |
DIALOG | Meets the minimum, but older than the latest |
| 10 | off | 2.0.0 |
3.0.0 |
DIALOG | Force is off, so the minimum is ignored; the latest decides |
Quick way to remember it:
- Want the wall?
force_update= on and minimum higher than the installed app. - Want the dialog?
latesthigher than the installed app. - Want to turn everything off? Put
force_update= off and set both versions to1.0.0+1(or empty).
Table 2: mistakes are ignored, never shown #
If a value is wrong, the app ignores the whole change and keeps the last good values. A typo can never lock your users out.
| # | What you set | What happens |
|---|---|---|
| 11 | Minimum is higher than latest (for example min 3.0.0, latest 2.0.0) |
Ignored. Nothing new is shown. |
| 12 | A version like abc, v2, 1.2.0-beta, 2.0. or 1..0 |
Ignored. Use only 1.2.3 or 1.2.3+4. |
| 13 | A version that is empty or only spaces | Counts as "not set" (not a mistake) |
Table 3: the store link (the Update button) #
The link is checked only when a wall or dialog is about to show.
| # | Platform | Store link | What happens |
|---|---|---|---|
| 14 | Android | empty | Works. The app builds the Play Store link from your package name. |
| 15 | iOS | empty, and an update is due | Nothing is shown. The change is ignored because the button would have nowhere to go. Always set ios_store_link. |
| 16 | iOS | empty, and no update is due | Nothing to show, nothing wrong |
| 17 | any | www.example.com, example.com (no https://) |
Ignored. The link must start with https://. |
| 18 | any | https://..., http://..., market://details?id=..., itms-apps://... |
Works. |
Table 4: which platform reads which values #
| Device | Reads | Notes |
|---|---|---|
| Android phone | android_version, android_latest_version, android_store_link |
force_update is shared by both phones |
| iPhone | ios_version, ios_latest_version, ios_store_link |
Same shared force_update |
| Web, Windows, Mac, Linux | nothing | They are never asked to update |
(With a prefix, the names start with it, for example customer_android_version. With
UpdateKeys.nested(), each platform has its own force_update.)
Table 5: what the user can do #
| Screen | Buttons | What each does |
|---|---|---|
| WALL | Update only | Opens the store. The back button does nothing. It cannot be closed. It goes away only after the user installs the new version. |
| DIALOG | Update | Opens the store |
| Later (or tap outside the box) | Hides the dialog for one day, then it comes back | |
| Skip this version | Hides it for this latest version for good. A newer latest version shows it again right away. |
Later and Skip never hide the wall.
Table 6: when does the user get your change? #
| Situation | What happens |
|---|---|
| The app starts | It reads your values. It waits at most 4 seconds. If the network is slow, the app opens anyway and the wall or dialog appears when the values arrive. |
| The app comes back to the screen after 1 hour or more | It reads again |
| Firebase | Firebase keeps a copy for up to 1 hour on release apps, so a change can take up to an hour. (Debug builds can fetch every time.) |
| An app that is already open | Do not rely on it. Close the app and open it again to see the change. |
| No internet | The app uses the last values it saved |
| It never got any values | It uses your defaults (which show nothing) |
| Your change has a mistake (Table 2) | Ignored. The last good values stay. |
Version format #
Write versions like 1.2.0 or 1.2.0+4 (name, then + and the build number).
These are the same numbers as version: 1.2.0+4 in your pubspec.yaml.
Letters like 1.2.0-beta are not allowed.
If you type a wrong version, the app ignores the bad update and keeps the last good values. It never blocks users because of a typo.
Use your own key names #
Your values can have other names. Tell the controller with keys:
keys: const UpdateKeys.prefixed('shop'), // shop_android_version, shop_force_update, ...
keys: const UpdateKeys.nested(), // {"android": {"minimum_version": ...}}
keys: const UpdateKeys( // anything you like
minimumVersion: 'min_{platform}', // {platform} becomes android or ios
latestVersion: 'new_{platform}',
storeUrl: 'link_{platform}',
forceUpdate: 'must_update',
),
UpdateKeys.prefixed('shop') is useful when two apps share one Firebase project.
Read other settings too #
The same values can hold your own settings (a feature switch, an API address...):
final values = AppUpdate.controller.values;
values.string('base_url', 'https://fallback.example.com'); // text
values.boolean('new_checkout'); // true / false
values.number('request_limit', 10); // number
If the value is missing or wrong, you get the fallback you gave. Do not read these
before await AppUpdate.controller.initialize() finishes.
Change the look #
UpdateGate(
controller: AppUpdate.controller,
loadingBuilder: (context) => const MySplash(),
requiredBuilder: (context, controller) => MyWall(controller: controller),
optionalBuilder: (context, controller) => MyDialog(controller: controller),
texts: const UpdateTexts(update: 'Get it now'), // Arabic text is automatic for `ar`
showOptional: true, // false = never show the optional dialog
allowSkip: true, // false = hide the "Skip this version" button
)
Your own wall only needs one button:
ListenableBuilder(
listenable: controller,
builder: (_, _) => FilledButton(
onPressed: controller.isOpeningUpdate ? null : controller.openUpdate,
child: Text(controller.actionError == null ? 'Update' : 'Try again'),
),
)
Problems? #
| What you see | What to check |
|---|---|
| Nothing shows after I changed a value | 1. Did you press Publish changes (Firebase)? 2. Is your number higher than the installed version? 3. For the wall: is force_update true? 4. Close the app fully and open it again. |
| Firebase says it is not initialized | Firebase.initializeApp(...) must run before the app first uses AppUpdate.controller. Put it first in main(). |
| The Update button does nothing | Check the store link. On iOS it is required. It must start with https://. |
| The wall shows for everyone | The minimum version is higher than the installed one. Put the old value back and publish. |
| Changes arrive slowly (Firebase) | Firebase keeps a copy for up to one hour on release apps. Wait, or restart the app after an hour. |
| The setup tool does not move with arrow keys | Update to the latest version of this package. If it still fails, type the number of your choice, or use --yes. |
dart run adaptable_remote_config says the package is missing |
Run flutter pub add adaptable_remote_config first, from the app folder. |
For developers #
Controller methods: initialize() (wait for the first check, at most 4 seconds),
refresh({force}) (ask the source again), check() (decide again, no network),
openUpdate() (open the store), dismissOptional({skipVersion}) (Later / Skip).
Controller values you can watch (it is a ChangeNotifier): values, update,
shouldPrompt, isReady, error, actionError.
Controller options: appId, provider, keys, defaults, storage, comparator,
openStore, startupWait, requestTimeout, refreshInterval, remindAfter,
subscribeToChanges, onError.
Sources: FirebaseConfigProvider, HttpConfigProvider, CustomConfigProvider.
Make your own by extending RemoteConfigProvider (one method: fetch()).
Building blocks: UpdateGate, RequiredUpdatePage, OptionalUpdateDialog,
UpdateButton, UpdateTexts.
Run the checks:
flutter pub get
flutter analyze
flutter test
Issues and pull requests are welcome at github.com/Abdelrahman1atef/adaptable_remote_config.
License #
MIT. See LICENSE.