fl_updater

pub package pub points license: MIT

A lightweight, cost-conscious Flutter plugin for Firebase Remote Config-driven app updates. Supports automatic launch checks, soft updates with per-version snoozing, non-dismissible force updates, and native App Store / Google Play Store redirection.


✨ Features

  • 🚀 Declarative Wrapper: Wrap your MaterialApp with FlUpdaterWrapper for zero-boilerplate launch checks.
  • Imperative API: Use FlUpdater().checkForUpdate() or FlUpdater().showUpdateDialog() for manual checks (e.g. from a settings screen).
  • 🔄 Soft & Force Updates:
    • Soft updates: Optional update prompt with a "Later" button.
    • Force updates: Mandatory blocking dialog (canPop: false) when the installed version is below fl_updater_min_version.
  • Smart Snoozing: Dismissing a soft update snoozes it for a configurable duration (default: 3 days). Snooze is scoped per version, so releasing a newer update immediately prompts the user again.
  • 💰 Cost-Conscious Architecture: Designed for Firebase Remote Config usage-based pricing:
    • Debug mode disabled by default: Prevents development hot restarts from consuming Remote Config quotas.
    • Cached fetches: Configurable minimumFetchInterval (default: 1 hour) ensures throttled network requests.
  • 🏬 Native Store Redirection: Opens the platform's native store page (Apple App Store on iOS, Google Play Store on Android).
  • 🎨 Fully Customizable UI: Style the built-in Material dialog with FlUpdaterDialogStyle, or supply your own custom UI via dialogBuilder.

📦 Installation

Add fl_updater and firebase_core to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  fl_updater: ^0.0.1
  firebase_core: ^4.13.0 # or latest

Then ensure Firebase is initialized in your main() method:

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

🔧 Firebase Remote Config Setup

Configure update parameters in your Firebase Console → Remote Config.

1. Parameters

Create the following two parameters:

Parameter Key Type Description Default Value
fl_updater_latest_version String The latest published version available in stores. "0.0.0"
fl_updater_min_version String The minimum supported version below which an update is forced. "0.0.0" (or Use in-app default)

2. Platform Conditional Values (fl_updater_android & fl_updater_ios)

In Firebase Console, you can define targeting conditions:

  • fl_updater_android: Condition rule: device.os == 'android'
  • fl_updater_ios: Condition rule: device.os == 'ios'

Then add conditional values to fl_updater_latest_version and fl_updater_min_version:

  • For Android (fl_updater_android): e.g., latest version "2.5.0", min version "2.0.0"
  • For iOS (fl_updater_ios): e.g., latest version "2.4.0", min version "2.1.0"

Firebase Remote Config automatically evaluates these conditions per device on fetch and serves the appropriate values to fl_updater. All other keys are ignored.

3. Store Redirection Identifiers

Store identifiers are configured directly in Dart code (via the wrapper or API call):

  • iOS (iosAppId): Numeric Apple App Store ID (e.g., '123456789').
  • Android (androidPackageId): Package name (e.g., 'com.example.app'). Defaults to the host app package name if omitted.

🚀 Usage

Wrap your MaterialApp with FlUpdaterWrapper inside the builder callback:

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, child) => FlUpdaterWrapper(
        iosAppId: '123456789',
        androidPackageId: 'com.example.app', // Optional: defaults to host package
        child: child!,
      ),
      home: const HomePage(),
    );
  }
}

This checks Remote Config once when the app is launched and displays the update dialog if an update is available and not currently snoozed.


2. Imperative / Manual Usage

Trigger an update check manually, such as from an "About" or "Settings" screen:

final updater = FlUpdater();

// Check and show dialog if an update is available:
await updater.showUpdateDialog(
  context,
  iosAppId: '123456789',
  androidPackageId: 'com.example.app',
);

Or check status without displaying a UI:

final updater = FlUpdater();
final info = await updater.checkForUpdate(
  iosAppId: '123456789',
);

print('Current: ${info.currentVersion}');
print('Latest: ${info.latestVersion}');
print('Status: ${info.status}'); // UpdateStatus.none, soft, or force

🎨 Customization

Styling the Default Dialog

Customize colors, typography, buttons, shapes, and icons using FlUpdaterDialogStyle:

FlUpdaterWrapper(
  iosAppId: '123456789',
  title: 'Exciting New Update!',
  message: 'We added new features and performance improvements.',
  updateButtonText: 'Update Now',
  laterButtonText: 'Not Now',
  style: FlUpdaterDialogStyle(
    backgroundColor: Colors.white,
    titleStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
    messageStyle: const TextStyle(color: Colors.black87),
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
    icon: const Icon(Icons.system_update, size: 40, color: Colors.blue),
  ),
  child: child!,
)

Providing a Custom Update UI (dialogBuilder)

Replace the built-in dialog entirely with your own custom widget or bottom sheet:

FlUpdaterWrapper(
  iosAppId: '123456789',
  dialogBuilder: (context, info, onUpdate, onLater) {
    final isForce = info.status == UpdateStatus.force;
    return AlertDialog(
      title: Text('Version ${info.latestVersion} Available'),
      content: Text('You are on ${info.currentVersion}. Please update to continue.'),
      actions: [
        if (!isForce)
          TextButton(
            onPressed: onLater, // Snoozes the update and dismisses dialog
            child: const Text('Remind me later'),
          ),
        ElevatedButton(
          onPressed: onUpdate, // Redirects to App/Play Store
          child: const Text('Update'),
        ),
      ],
    );
  },
  child: child!,
)

⏰ Snoozing Behavior

When a soft (optional) update is available, tapping the "Later" button snoozes update prompts for snoozeDuration (default: 3 days).

  • The snooze is persisted locally via SharedPreferences.
  • Snoozes are scoped to the latest version. When you publish a newer version in Remote Config, the active snooze is automatically invalidated.
  • Force updates always bypass snooze and cannot be dismissed.
FlUpdaterWrapper(
  snoozeDuration: const Duration(days: 7), // Snooze for 1 week
  child: child!,
)

Resetting Snooze (For Debugging & Testing)

You can automatically clear the snooze store on every app launch during development:

FlUpdaterWrapper(
  enableInDebugMode: true,
  clearSnoozeInDebugMode: true, // Clears previous snoozes on app launch in debug mode
  child: child!,
)

Or reset it manually via code:

// Globally clear active snooze state:
await FlUpdater.clearSnoozeStore();

// Or on an instance:
final updater = FlUpdater();
await updater.clearSnooze();

💰 Fetch Behavior & Quota Optimization

To safeguard your Firebase Remote Config quota and avoid unintended billing:

  1. Disabled in Debug Mode by Default: kDebugMode disables Remote Config fetching entirely so that frequent hot restarts do not burn quotas. To test in debug mode or staging, pass enableInDebugMode: true:
    FlUpdaterWrapper(
      enableInDebugMode: true, // Opt-in for debug/staging builds
      child: child!,
    )
    
  2. Fetch Interval Throttling: The minimumFetchInterval (default: 1 hour) prevents frequent network queries. Repeated checks within this duration use the Firebase cached values.
    FlUpdaterWrapper(
      minimumFetchInterval: const Duration(minutes: 30),
      child: child!,
    )
    

⚡ Real-Time Remote Config Updates

fl_updater listens to Firebase Remote Config updates in real time via onConfigUpdated:

  • When you publish changes to fl_updater_latest_version or fl_updater_min_version in the Firebase Console, the new config is activated immediately.
  • The update status is evaluated without waiting for minimumFetchInterval to expire.
  • Active snoozes are automatically cleared so users are prompted for the newly published version right away.
  • If the new version requires an update, the update dialog appears instantly for active users.

Real-time updates are enabled by default (listenForRealtimeUpdates: true). You can disable them if needed:

FlUpdaterWrapper(
  listenForRealtimeUpdates: false, // Only check on app launch
  child: child!,
)

🪵 Diagnostic Logging

Logging is disabled by default to keep console and production outputs clean. You can enable diagnostic logging in several ways:

1. Globally

void main() {
  FlUpdater.enableLogging = true;
  runApp(const MyApp());
}

2. Per Wrapper or Method Call

FlUpdaterWrapper(
  enableLogging: true,
  child: child!,
)

📖 API Reference

FlUpdaterWrapper & FlUpdater.showUpdateDialog

Property Type Default Description
iosAppId String? null Numeric Apple App Store ID (required for iOS).
androidPackageId String? null Google Play Store package name (defaults to host app).
navigatorKey GlobalKey<NavigatorState>? null Optional explicit key for the root Navigator.
snoozeDuration Duration Duration(days: 3) How long to snooze soft updates when dismissed.
minimumFetchInterval Duration Duration(hours: 1) Throttling interval for Firebase Remote Config fetches.
enableInDebugMode bool false Enable checks in kDebugMode.
clearSnoozeInDebugMode bool false Automatically clear saved snooze state on launch in debug mode.
listenForRealtimeUpdates bool true Instantly activate and check updates on Remote Config publish.
enableLogging bool? null Enable diagnostic console logs for troubleshooting.
title String? 'Update available' Dialog title text.
message String? null Dialog message body text.
updateButtonText String? 'Update' Label for the update button.
laterButtonText String? 'Later' Label for the later/snooze button.
style FlUpdaterDialogStyle? null Style configuration for the default dialog.
dialogBuilder FlUpdaterDialogBuilder? null Custom builder to provide your own dialog UI.

📱 Example App

Check out the example directory for a complete sample app demonstrating both automatic wrapper and manual checking with Firebase Remote Config.

To run the example app:

cd example
flutter run

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

Libraries

fl_updater
A Flutter plugin that provides Firebase Remote Config-driven app update checking, snoozable soft updates, blocking force updates, and native App Store / Google Play Store redirection.