惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
H
Hacker News: Front Page
N
News and Events Feed by Topic
Know Your Adversary
Know Your Adversary
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
K
Kaspersky official blog
Forbes - Security
Forbes - Security
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
P
Privacy & Cybersecurity Law Blog
H
Heimdal Security Blog
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
V
Vulnerabilities – Threatpost
T
Tenable Blog
T
Tailwind CSS Blog
P
Privacy International News Feed
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
博客园 - Franky
Hacker News: Ask HN
Hacker News: Ask HN
Jina AI
Jina AI
C
Cybersecurity and Infrastructure Security Agency CISA
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
Vercel News
Vercel News
A
About on SuperTechFans
爱范儿
爱范儿
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
The Last Watchdog
The Last Watchdog
Engineering at Meta
Engineering at Meta
Spread Privacy
Spread Privacy
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - 司徒正美
量子位
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
Project Zero
Project Zero

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Динамические иконки приложения на Flutter: подробная инструкция для ручного выбора и обновлений по воздуху
Павел Гершевич · 2026-06-25 · via Все публикации подряд на Хабре

Средний

10 мин

109

Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead агентства продуктовой разработки Amiga и соавтор книги "Основы Flutter". Мы с командой подготовили для вас перевод статьи о том, как можно кастомизировать иконку приложения динамически. Всем приятного чтения!

Наличие приложения на домашнем экране устройства очень ценно для бизнеса. Когда у пользователей есть возможность самим настраивать иконку вручную, это дарит им приятное чувство индивидуальности. Вместе с этим, возможность менять иконку приложения удаленно позволяет внедрять тематические изменения. Например, на различные праздники, ребрендинг в темную тему или временную картинку с маркетинговой компанией. И все это без публикации новой сборки в App Store или Google Play Store.

В этой подробной инструкции мы реализуем систему для динамических иконок в Flutter, которая работает в двух режимах, не полагаясь на багованные или устаревшие сторонние библиотеки. Мы создадим собственные MethodChannels на Kotlin и Swift с поддержкой двух ключевых функций:

  1. Ручной выбор: предоставление пользователям возможности самим выбрать иконку по их предпочтениям в настройках UI.

  2. Обновление по воздуху: переопределение или синхронизация иконки автоматически через Firebase Remote Config.

Как это устроено

Перед написанием кода, важно понять как отличаются платформы, для которых мы это делаем. Так как Flutter работает в едином контексте выполнения, это означает, что мы должны использовать платформенные каналы для запуска нативных API операционной системы.

Android: Component Aliases

Android обрабатывает иконки приложения статично при помощи AndroidManifest.xml. Чтобы обойти эти ограничения, мы объявляем несколько тегов <activity-alias>, которые указывают на MainActivity. Переключая состояние этих alias при помощи нативного PackageManager, домашний экран устройства обновляется для отображения иконки, добавленной к активному alias.

⚠️ Побочный эффект на Android:

Включение или выключение activity alias очищает текущий стек приложений и кратковременно отключает фоновое состояние приложения, чтобы переключить профиль запуска.

iOS: API альтернативных иконок

Apple предоставляет нативный способ через UIApplication.shared.setAlternateIconName(). В отличии от стандартных графических пакетов, альтернативные иконки не могут быть добавлены в компилируемый каталог ассетов (Assets.xcassets). Вместо этого, они должны быть добавлены в корневую папку приложения как отдельные файлы и прописаны вручную внутри настроек приложения (Info.plist).

Шаг 1: Подготавливаем нативную инфраструктуру

1. Подготовка MethodChannel в Flutter

Создайте удобный вспомогательный класс в вашем Dart проекте для того, чтобы подключиться к каналу связи с нативной системой.

import 'package:flutter/services.dart';

class DynamicIconService {
 static const MethodChannel _channel = MethodChannel('com.dynamicicon.app/dynamic_icon');
 /// Необходимо добавить нативный код для смены активной иконки приложения.
 static Future<void> changeIcon(String iconName) async {
   try {
     await _channel.invokeMethod('setIcon', {'iconName': iconName});
   } on PlatformException catch (e) {
     print("Failed to change application launcher profile: '${e.message}'.");
   }
 }
}

2. Интеграция Android (AndroidManifest.xml и Kotlin)

Сначала, добавьте альтернативные графические ассеты в ваши папки с ресурсами:

  • android/app/src/main/res/mipmap-hdpi/ic_launcher_dark.png

  • android/app/src/main/res/mipmap-hdpi/ic_launcher_festive.png

Далее, отредактируйте android/app/src/main/AndroidManifest.xml - поставьте параметру android:enabled у .MainActivity значение false. Так мы передадим все хуки изначальной системы нашим alias.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
   <application
       android:label="Dynamic App"
       android:name="${applicationName}">
      
       <!-- Base Target Activity Framework (Deactivated) -->
       <activity
           android:name=".MainActivity"
           android:exported="true"
           android:enabled="false"
           android:launchMode="singleTop"
           android:theme="@style/LaunchTheme">
           <meta-data
               android:name="io.flutter.embedding.android.NormalTheme"
               android:resource="@style/NormalTheme" />
       </activity>

       <!-- Default Application Icon Alias -->
       <activity-alias
           android:name=".DefaultIcon"
           android:enabled="true"
           android:exported="true"
           android:icon="@mipmap/ic_launcher"
           android:targetActivity=".MainActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN"/>
               <category android:name="android.intent.category.LAUNCHER"/>
           </intent-filter>
       </activity-alias>

       <!-- Dark Variant Icon Alias -->
       <activity-alias
           android:name=".DarkIcon"
           android:enabled="false"
           android:exported="true"
           android:icon="@mipmap/ic_launcher_dark"
           android:targetActivity=".MainActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN"/>
               <category android:name="android.intent.category.LAUNCHER"/>
           </intent-filter>
       </activity-alias>

       <!-- Festive Variant Icon Alias -->
       <activity-alias
           android:name=".FestiveIcon"
           android:enabled="false"
           android:exported="true"
           android:icon="@mipmap/ic_launcher_festive"
           android:targetActivity=".MainActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN"/>
               <category android:name="android.intent.category.LAUNCHER"/>
           </intent-filter>
       </activity-alias>
   </application>
</manifest>

Откройте файл MainActivity.kt для получения методов из MethodChannel и запуска цикла смены состояния:

package com.dynamicicon.app

import android.content.ComponentName
import android.content.pm.PackageManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
   private val CHANNEL = "com.dynamicicon.app/dynamic_icon"
   private val packageNamespace = "com.dynamicicon.app"
   private val aliasRegistry = listOf("DefaultIcon", "DarkIcon", "FestiveIcon")
   override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
       super.configureFlutterEngine(flutterEngine)
      
       MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
           if (call.method == "setIcon") {
               val targetIcon = call.argument<String>("iconName")
               if (targetIcon != null && aliasRegistry.contains(targetIcon)) {
                   toggleLauncherComponent(targetIcon)
                   result.success(null)
               } else {
                   result.error("ALIAS_NOT_FOUND", "The target alias mapping configuration is missing.", null)
               }
           } else {
               result.notImplemented()
           }
       }
   }
   private fun toggleLauncherComponent(targetAlias: String) {
       val pm = applicationContext.packageManager
      
       for (alias in aliasRegistry) {
           val componentState = if (alias == targetAlias) {
               PackageManager.COMPONENT_ENABLED_STATE_ENABLED
           } else {
               PackageManager.COMPONENT_ENABLED_STATE_DISABLED
           }
          
           pm.setComponentEnabledSetting(
               ComponentName(applicationContext, "$packageNamespace.$alias"),
               componentState,
               PackageManager.DONT_KILL_APP
           )
       }
   }
}

3. Интеграция iOS (Info.plist и Swift)

Положите файлы ваших альтернативных иконок прямо в папку ios/Runner/ в виде отдельных исходников (например, ic_launcher_dart@2x.png, ic_launcher_festive@3x.png).

Добавьте эти ключи в ios/Runner/Info.plist:

<key>CFBundleIcons</key>
<dict>
   <key>CFBundlePrimaryIcon</key>
   <dict>
       <key>CFBundleIconFiles</key>
       <array>
           <string>AppIcon</string>
       </array>
   </dict>
   <key>CFBundleAlternateIcons</key>
   <dict>
       <key>DarkIcon</key>
       <dict>
           <key>CFBundleIconFiles</key>
           <array>
               <string>ic_launcher_dark</string>
           </array>
           <key>UIPrerenderedIcon</key>
           <false/>
       </dict>
       <key>FestiveIcon</key>
       <dict>
           <key>CFBundleIconFiles</key>
           <array>
               <string>ic_launcher_festive</string>
           </array>
           <key>UIPrerenderedIcon</key>
           <false/>
       </dict>
   </dict>
</dict>

Откройте ios/Runner/AppDelegate.swift и настройте логику MethodChannel:

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
 override func application(
   _ application: UIApplication,
   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
 ) -> Bool {
  
   let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
   let iconChannel = FlutterMethodChannel(name: "com.dynamicicon.app/dynamic_icon",
                                          binaryMessenger: controller.binaryMessenger)
  
   iconChannel.setMethodCallHandler({
     (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
     guard call.method == "setIcon" else {
         result(FlutterMethodNotImplemented)
         return
     }
    
     if let arguments = call.arguments as? [String: Any],
        let iconName = arguments["iconName"] as? String {
         self.updateAlternateIcon(named: iconName, response: result)
     } else {
         result(FlutterError(code: "BAD_ARGS", message: "Arguments malformed", details: nil))
     }
   })
   GeneratedPluginRegistrant.register(with: self)
   return super.application(application, didFinishLaunchingWithOptions: launchOptions)
 }
 private func updateAlternateIcon(named targetName: String, response: @escaping FlutterResult) {
     if #available(iOS 10.3, *) {
         let systemKey = (targetName == "DefaultIcon") ? nil : targetName
        
         UIApplication.shared.setAlternateIconName(systemKey) { error in
             if let error = error {
                 response(FlutterError(code: "IOS_ERROR", message: error.localizedDescription, details: nil))
             } else {
                 response(null)
             }
         }
     } else {
         response(FlutterError(code: "UNSUPPORTED_OS", message: "Minimum OS layout required", details: nil))
     }
 }
}

Шаг 2: Добавим логику управления иконками

Теперь, когда наш нативный код готов, нам нужно сбалансировать ручной выбор при помощи обновлений через remote config.

Для того, чтобы решить эту проблему быстро, мы установим бизнес-правило: Firebase Remote Config будет работать как глобальное переопределение. Если ключ из remote config опубликован через облачную админку, то он переписывает то, что пользователь установил вручную. Если облачная конфигурация не задана или возвращает DefaultIcon, то приложение должно использовать ручной выбор, который пользователь сделал внутри приложения.

Сначала, добавьте зависимости в ваш pubspec.yaml:

dependencies:
 flutter:
   sdk: flutter
 firebase_core: ^3.0.0
 firebase_remote_config: ^5.0.0
 shared_preferences: ^2.2.0

Механизм интеграции двойного управления

Создайте файл app_icon_manager.dart, чтобы четко оркестрировать оба источника данных:

import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dynamic_icon_service.dart';

class AppIconManager {
 static const String _remoteOverrideKey = 'cloud_icon_override';
 static const String _userPrefKey = 'user_selected_icon';
 static const String _activeCachedKey = 'currently_applied_icon';

 /// Режим 1: Работает вручную, когда пользователь нажимает на иконку в настройках приложения
 static Future<void> updateUserIconPreference(String targetIconName) async {
   final SharedPreferences prefs = await SharedPreferences.getInstance();
   await prefs.setString(_userPrefKey, targetIconName);
  
   // Проверка на блокирования облаком перед обновлением
   final FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.instance;
   final String cloudOverride = remoteConfig.getString(_remoteOverrideKey);
   if (cloudOverride == 'DefaultIcon' || cloudOverride.isEmpty) {
     await _applyIconSafetyGuard(targetIconName);
   } else {
     print("User setting cached, but blocked by active Cloud Override: $cloudOverride");
   }
 }

 /// Режим 2: Работает автоматически при запуске приложения для того, чтобы получить правила по воздуху
 static Future<void> syncCloudConfiguration() async {
   final FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.instance;
   try {
     await remoteConfig.setDefaults(<String, dynamic>{_remoteOverrideKey: 'DefaultIcon'});
     await remoteConfig.setConfigSettings(RemoteConfigSettings(
       fetchTimeout: const Duration(seconds: 10),
       minimumFetchInterval: const Duration(hours: 2),
     ));
     bool updated = await remoteConfig.fetchAndActivate();
     if (!updated) return;
     final String cloudOverrideValue = remoteConfig.getString(_remoteOverrideKey);
     final SharedPreferences prefs = await SharedPreferences.getInstance();
     if (cloudOverrideValue != 'DefaultIcon' && cloudOverrideValue.isNotEmpty) {
       // Облачные данные перезаписывают данные пользователя
       await _applyIconSafetyGuard(cloudOverrideValue);
     } else {
       // Fallback на выбор пользователя
       final String userPreference = prefs.getString(_userPrefKey) ?? 'DefaultIcon';
       await _applyIconSafetyGuard(userPreference);
     }
   } catch (e) {
     print('Failed to synchronize Firebase Remote Config layer: $e');
   }
 }

 /// Проверка и запуск MethodChannel если иконка действительно изменена
 static Future<void> _applyIconSafetyGuard(String targetValue) async {
   final SharedPreferences prefs = await SharedPreferences.getInstance();
   final String currentlyActive = prefs.getString(_activeCachedKey) ?? 'DefaultIcon';
   // Защита от лишних действий
   if (currentlyActive == targetValue) return;
   print('Modifying launcher layout state from $currentlyActive to $targetValue');
   await DynamicIconService.changeIcon(targetValue);
   await prefs.setString(_activeCachedKey, targetValue);
 }
}

Шаг 3: Добавление на UI слой и запуск жизненного цикла приложения

Теперь, инициализируйте наш слой синхронизации внутри метода main() вашего приложения и сделайте интерфейс быстрого выбора.

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

void main() async {
 WidgetsFlutterBinding.ensureInitialized();
 await Firebase.initializeApp();
 // Запуск при старте приложения
 await AppIconManager.syncCloudConfiguration();
 runApp(const MyApp());
}
class MyApp extends StatelessWidget {
 const MyApp({super.key});
 @override
 Widget build(BuildContext context) {
   return const MaterialApp(
     home: SettingsIconScreen(),
   );
 }
}
class SettingsIconScreen extends StatelessWidget {
 const SettingsIconScreen({super.key});
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: const Text('Customize App Icon')),
     body: Padding(
       padding: const EdgeInsets.all(16.0),
       child: Column(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           const Text(
             'Select Your Preference:',
             style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
           ),
           const SizedBox(height: 12),
           ListTile(
             leading: const Icon(Icons.phone_android),
             title: const Text('Default Classic Blue'),
             onTap: () => AppIconManager.updateUserIconPreference('DefaultIcon'),
           ),
           ListTile(
             leading: const Icon(Icons.dark_mode),
             title: const Text('Premium Minimalist Dark'),
             onTap: () => AppIconManager.updateUserIconPreference('DarkIcon'),
           ),
           ListTile(
             leading: const Icon(Icons.celebration),
             title: const Text('Festive Celebration Theme'),
             onTap: () => AppIconManager.updateUserIconPreference('FestiveIcon'),
           ),
         ],
       ),
     ),
   );
 }
}

Заключение. Управление из облака

Наше приложение полностью готово к двойному управлению! Для того, чтобы запустить глобальное обновление по воздуху:

  1. Откройте Консоль Firebase и перейдите к Remote Config.

  2. Создайте параметр cloud_icon_override.

  3. Поставьте ему значение FestiveIcon (или оставьте DefaultIcon, если хотите учитывать выбор иконки пользователям).

  4. Нажмите Опубликовать изменения.