dynamic_app_icon_changer 0.0.3 copy "dynamic_app_icon_changer: ^0.0.3" to clipboard
dynamic_app_icon_changer: ^0.0.3 copied to clipboard

A Flutter plugin for changing app icons dynamically at runtime on Android and iOS.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:dynamic_app_icon_changer/dynamic_app_icon_changer.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dynamic Icon Changer Demo',
      theme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _supportsAlternate = false;
  String? _currentIconName;
  String _statusMessage = '';
  int _badgeNumber = 0;
  final TextEditingController _badgeController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _loadState();
    _registerProtectedComponents();
  }

  @override
  void dispose() {
    _badgeController.dispose();
    super.dispose();
  }

  /// Register any third-party Android components that should be protected
  /// during icon switches. This ensures components like push notification
  /// handlers are not disrupted when activity-alias states change.
  Future<void> _registerProtectedComponents() async {
    if (!Platform.isAndroid) return;

    try {
      // Example: protect MoEngage PushTracker from being disabled during
      // icon switches. Uncomment and adjust the class name for your app:
      //
      // await DynamicAppIconChanger.registerProtectedComponents([
      //   const ProtectedComponent(
      //     className: 'com.moengage.pushbase.activities.PushTracker',
      //     desiredState: ComponentState.enabled,
      //   ),
      // ]);
    } catch (e) {
      debugPrint('Failed to register protected components: $e');
    }
  }

  Future<void> _loadState() async {
    try {
      final supports = await DynamicAppIconChanger.supportsAlternateIcons;
      final iconName = await DynamicAppIconChanger.alternateIconName;
      int badge = 0;
      if (Platform.isIOS) {
        badge = await DynamicAppIconChanger.badgeNumber;
      }
      if (!mounted) return;
      setState(() {
        _supportsAlternate = supports;
        _currentIconName = iconName;
        _badgeNumber = badge;
        _statusMessage = 'Ready. Supports alternate icons: $supports';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() {
        _statusMessage = 'Error loading state: ${e.message}';
      });
    }
  }

  Future<void> _setIcon(String? iconName) async {
    try {
      await DynamicAppIconChanger.setAlternateIconName(iconName);
      final current = await DynamicAppIconChanger.alternateIconName;
      if (!mounted) return;
      setState(() {
        _currentIconName = current;
        _statusMessage =
            'Icon changed to: ${iconName ?? "Default"} successfully!';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      _showError('Failed to change icon: ${e.message}');
      setState(() {
        _statusMessage = 'Error: ${e.message}';
      });
    }
  }

  Future<void> _setIconBlacklisted() async {
    try {
      // Blacklist the current device manufacturer so the change is skipped
      await DynamicAppIconChanger.setAlternateIconName(
        'IconBlue',
        blacklistedBrands: [
          // This will match the current device, so the change should be skipped
          if (Platform.isAndroid) 'auto-detected',
        ],
      );
      if (!mounted) return;
      setState(() {
        _statusMessage =
            'Blacklist demo: Attempted icon change with current manufacturer blacklisted. '
            'Check if the icon actually changed.';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      _showError('Blacklist demo error: ${e.message}');
    }
  }

  Future<void> _setBadge(int count) async {
    try {
      await DynamicAppIconChanger.setBadgeNumber(count);
      final current = await DynamicAppIconChanger.badgeNumber;
      if (!mounted) return;
      setState(() {
        _badgeNumber = current;
        _statusMessage = 'Badge set to $count';
      });
    } on PlatformException catch (e) {
      if (!mounted) return;
      _showError('Failed to set badge: ${e.message}');
    }
  }

  void _showError(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: Colors.red.shade700,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dynamic Icon Changer'),
        centerTitle: true,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // Status card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Current Icon',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Icon(
                        Icons.app_settings_alt,
                        color: Theme.of(context).colorScheme.primary,
                      ),
                      const SizedBox(width: 8),
                      Text(
                        _currentIconName ?? 'Default',
                        style: Theme.of(context).textTheme.headlineSmall,
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Text(
                    _statusMessage,
                    style: Theme.of(context).textTheme.bodySmall?.copyWith(
                          color: Theme.of(context).colorScheme.outline,
                        ),
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 24),

          // Icon switching section
          Text(
            'Switch App Icon',
            style: Theme.of(context).textTheme.titleLarge,
          ),
          const SizedBox(height: 12),

          _IconButton(
            label: 'Default Icon',
            icon: Icons.home,
            color: Colors.grey,
            isActive: _currentIconName == null,
            onPressed: _supportsAlternate ? () => _setIcon(null) : null,
          ),
          const SizedBox(height: 8),
          _IconButton(
            label: 'Blue Icon',
            icon: Icons.circle,
            color: Colors.blue,
            isActive: _currentIconName == 'IconBlue',
            onPressed: _supportsAlternate ? () => _setIcon('IconBlue') : null,
          ),
          const SizedBox(height: 8),
          _IconButton(
            label: 'Green Icon',
            icon: Icons.circle,
            color: Colors.green,
            isActive: _currentIconName == 'IconGreen',
            onPressed: _supportsAlternate ? () => _setIcon('IconGreen') : null,
          ),

          // Android blacklist demo
          if (Platform.isAndroid) ...[
            const SizedBox(height: 24),
            Text(
              'Android: Blacklist Demo',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Text(
              'This button tries to switch the icon but blacklists the current '
              'device manufacturer, so the change should be silently skipped.',
              style: Theme.of(context).textTheme.bodySmall,
            ),
            const SizedBox(height: 8),
            FilledButton.tonal(
              onPressed: _setIconBlacklisted,
              child: const Text('Try Blacklisted Change'),
            ),
          ],

          // iOS badge section
          if (Platform.isIOS) ...[
            const SizedBox(height: 24),
            Text(
              'iOS: Badge Number',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Text(
              'Current badge: $_badgeNumber',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _badgeController,
                    keyboardType: TextInputType.number,
                    decoration: const InputDecoration(
                      labelText: 'Badge number',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                FilledButton(
                  onPressed: () {
                    final count =
                        int.tryParse(_badgeController.text.trim()) ?? 0;
                    _setBadge(count);
                  },
                  child: const Text('Set Badge'),
                ),
              ],
            ),
            const SizedBox(height: 8),
            OutlinedButton(
              onPressed: () => _setBadge(0),
              child: const Text('Clear Badge'),
            ),
          ],
        ],
      ),
    );
  }
}

class _IconButton extends StatelessWidget {
  const _IconButton({
    required this.label,
    required this.icon,
    required this.color,
    required this.isActive,
    this.onPressed,
  });

  final String label;
  final IconData icon;
  final Color color;
  final bool isActive;
  final VoidCallback? onPressed;

  @override
  Widget build(BuildContext context) {
    return FilledButton.tonal(
      onPressed: onPressed,
      style: FilledButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        side: isActive
            ? BorderSide(
                color: Theme.of(context).colorScheme.primary, width: 2)
            : null,
      ),
      child: Row(
        children: [
          Icon(icon, color: color),
          const SizedBox(width: 12),
          Expanded(child: Text(label)),
          if (isActive)
            Icon(
              Icons.check_circle,
              color: Theme.of(context).colorScheme.primary,
            ),
        ],
      ),
    );
  }
}
2
likes
150
points
0
downloads

Documentation

API reference

Publisher

verified publishersrkstudios.art

Weekly Downloads

A Flutter plugin for changing app icons dynamically at runtime on Android and iOS.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on dynamic_app_icon_changer

Packages that implement dynamic_app_icon_changer