Если вы предпочитаете видеоконтент, посмотрите видеоверсию этой статьи на YouTube (EN).

Что такое Firebase Remote Config?

Firebase Remote Config – это облачная служба, которая позволяет вам изменять поведение и внешний вид вашего приложения, не требуя обновления приложения. Вы можете вносить изменения в свое приложение в режиме реального времени, и пользователи сразу увидят обновления. Это особенно полезно для A/B-тестирования, пометки функций и других экспериментов.

В этой статье я затрону следующие темы:

  • Значения конфигурации приложения;

  • Уведомление пользователей о новой версии приложения;

  • Пометка функций (прим.: Feature flagging);

  • Поэтапное развертывание;

  • А/Б тестирование.

Кажется, что это очень много! Начнем с обзора демо-приложения.

Flutter Forward agenda приложение

Flutter Forward agenda – это простое приложение, которое отображает общую информацию о конференции и расписание мероприятий.

Обзор

Информация о событии в настоящее время считывается из файла JSON, хранящегося в локальных ресурсах.

В приложении есть функция прямой трансляции, которая уведомляет пользователя о начале трансляции и предоставляет ссылку для присоединения.

Кроме того, есть два разных доступных варианта добавления сеанса в список избранного — либо с помощью скользящей кнопки, либо с помощью кнопки на карточке. Мы вернемся к этому чуть позже, когда будем говорить об A/B-тестировании.

Наконец, реализован модальный диалог обновления приложения, который уведомляет пользователей о выходе новой версии и узнает, является ли обновление обязательным.

Проблемы

В целом, приложение работает отлично, однако в процессе работы могут возникнуть некоторые проблемы:

  1. Данные хранятся локально. Это здорово – нам не нужно подключение к Интернету, верно? В настоящее время в расписании есть несколько пунктов TBA, и в будущем сроки проведения мероприятия могут измениться. То есть, как только мы обновим расписание мероприятий, нам придется выпустить новую версию приложения только для обновления данных. Это не идеальный вариант, поскольку нам придётся ждать завершения процесса рассмотрения приложения, а затем отправлять обновление пользователям. Это может занять некоторое время, и пользователи могут даже пропустить событие!

  2. Такие функции, как обновление приложения или уведомление о прямой трансляции, не могут существовать без их удаленного запуска. Технически мы могли бы реализовать триггеры на устройствах, но столкнулись бы с той же проблемой, что и в случае с данными – нам нужно выпустить новую версию приложения только для того, чтобы изменить условную логику.

  3. На данный момент, как мы можем проверить использование функции добавления в избранное? Приложение должно предоставлять некую аналитику и функции A/B тестирования, чтобы подтвердить наши предположения и обеспечить наилучший опыт для наших пользователей.

К счастью, Firebase Remote Config кажется идеально подходящим для таких случаев использования. Давайте начнем!

Настройка Firebase

Начать лучше всего с создания нового проекта Firebase и добавления его в приложение Flutter. Перейдите в Firebase console, выберите "Добавить проект" и придумайте не очень креативное название проекта.

Не забудьте включить Google Analytics для проекта, поскольку чуть позже мы будем использовать его для A/B-тестирования.

Затем выберите аккаунт Google Analytics и выберите "Создать проект". Через минуту или две ваш проект должен быть готов к использованию в вашем Flutter-проекте.

Теперь давайте добавим Firebase в приложение Flutter. В этом проекте мы будем использовать пакеты firebase_analytics, firebase_core и firebase_remote_config, поэтому добавьте их в зависимости:

# pubspec.yaml
firebase_analytics: ^10.1.0
firebase_core: ^2.4.1
firebase_remote_config: ^3.0.9

????‍♂️ Инфо

Представленные выше версии являются последними на момент написания этой статьи. Обязательно проверяйте последние версии на pub.dev.

Самый простой способ связать проект Firebase с приложением Flutter – использовать Flutterfire CLI. Выполните следующую команду (замените <project_id> на ID вашего проекта Firebase), чтобы настроить проект:

flutterfire configure -p <project_id>

Команда flutterfire configure инициализирует проект Firebase для выбранных платформ. Поскольку мы еще не инициализировали Android или iOS приложения, то они будут созданы за нас автоматически.

Наконец, мы инициализируем Firebase при запуске приложения:

// main.dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  runApp(
    ProviderScope(
      child: const App(),
    ),
  );
}

???? Осторожно

Важно добавить WidgetsFlutterBinding.ensureInitialized() перед инициализацией Firebase. Это необходимо потому, что Firebase.initializeApp() должен вызывать нативный код, используя каналы платформы для инициализации Firebase, и этот процесс является асинхронным. Поэтому нам необходимо убедиться, что WidgetsBinding инициализирован.

Использование параметров Firebase Remote Config

Первое улучшение, которое мы сделаем для приложения Flutter Forward agenda, – это извлечение локальных данных о событиях в Firebase Remote Config, чтобы их можно было обновлять на лету.

В консоли Firebase Remote Config создайте первое свойство под названием event_info, которое имеет тип JSON. Затем скопируйте данные о событиях из локального файла активов в приложении Flutter и вставьте их в представление редактора JSON.

Не забудьте сохранить изменения и опубликовать свойства Firebase Remote Config в первый раз. ????

Теперь мы можем безопасно удалить активы из проекта и приступить к реализации FirebaseRemoteConfigService. Этот сервис является оберткой вокруг зависимости FirebaseRemoteConfig, которая будет использоваться во всем приложении. Во-первых, добавьте несколько шаблонов кода для Riverpod, чтобы сделать сервис доступным для всего приложения. Затем приступите к реализации кода инициализации, создав блок try/catch для обработки некоторых ошибок Firebase, если таковые будут иметь место.

// firebase_remote_config_service.dart
import 'dart:developer' as developer;

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'firebase_remote_config_service.g.dart';

@riverpod
FirebaseRemoteConfigService firebaseRemoteConfigService(_) {
  throw UnimplementedError();
}

class FirebaseRemoteConfigService {
  const FirebaseRemoteConfigService({
    required this.firebaseRemoteConfig,
  });

  final FirebaseRemoteConfig firebaseRemoteConfig;

  Future<void> init() async {
    try {
      // <...>
    } on FirebaseException catch (e, st) {
      developer.log(
        'Unable to initialize Firebase Remote Config',
        error: e,
        stackTrace: st,
      );
    }
  }
}

Чтобы правильно инициализировать удаленный конфиг, нам нужно

  • убедиться, что последний активированный конфиг доступен для геттеров,

  • установить параметры конфигурации и

  • выбрать подходящую стратегию загрузки.

Также добавьте геттер для JSON информации о событии:

// firebase_remote_config_service.dart
class FirebaseRemoteConfigService {
  // <...>

  Future<void> init() async {
    try {
      await firebaseRemoteConfig.ensureInitialized();
      await firebaseRemoteConfig.setConfigSettings(
        RemoteConfigSettings(
          fetchTimeout: const Duration(seconds: 10),
          minimumFetchInterval: Duration.zero,
        ),
      );
      await firebaseRemoteConfig.fetchAndActivate();
    } on FirebaseException catch (e, st) {
      // <...>
    }
  }

  String getEventInfoJson() => firebaseRemoteConfig.getString('event_info');
}

???? Подсказка

Обратитесь к документации Firebase, чтобы узнать больше о различных стратегиях загрузки Firebase Remote Config и о том, какую из них следует использовать в вашем конкретном случае.

Не забудьте инициализировать проект в main-методе вашего проекта:

// main.dart
// <...>
import 'package:firebase_remote_config/firebase_remote_config.dart';

import 'features/firebase/firebase_remote_config_service.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  final firebaseRemoteConfigService = FirebaseRemoteConfigService( // add
    firebaseRemoteConfig: FirebaseRemoteConfig.instance,
  );
  await firebaseRemoteConfigService.init(); // add

  runApp(
    ProviderScope(
      overrides: [ // add
        firebaseRemoteConfigServiceProvider.overrideWith(
          (_) => firebaseRemoteConfigService,
        ),
      ],
      child: const App(),
    ),
  );
}

Наконец, нам нужно обновить источник данных о событиях внутри EventRepository. Вместо загрузки данных JSON из локальных активов мы используем значение из Firebase Remote Config:

// event_repository.dart
import 'dart:convert';

import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../firebase/firebase_remote_config_service.dart'; // add
import 'models/event_info.dart';

part 'event_repository.g.dart';

@riverpod
EventRepository eventRepository(EventRepositoryRef ref) {
  return EventRepository(
    firebaseRemoteConfigService: ref.watch(firebaseRemoteConfigServiceProvider), // add
  );
}

@riverpod
Future<EventInfo> eventInfo(EventInfoRef ref) {
  return ref.watch(eventRepositoryProvider).getEventInfo();
}

class EventRepository {
  const EventRepository({
    required this.firebaseRemoteConfigService, // add
  });

  final FirebaseRemoteConfigService firebaseRemoteConfigService; // add

  Future<EventInfo> getEventInfo() async {
    final json = firebaseRemoteConfigService.getEventInfoJson(); // add

    return EventInfo.fromJson(jsonDecode(json) as Map<String, dynamic>);
  }
}

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

В следующем разделе мы реализуем функцию обновления приложения.

Использование "условий" в Firebase Remote Config

Чтобы разграничить значения Firebase Remote Config в зависимости от платформы, местоположения, группы пользователей и других критериев, мы используем "условия". Для приложения Flutter Forward agenda мы будем использовать условия для реализации функций обновления приложения и живых уведомлений.

Модальное окно обновления приложения

Дело в том, что иногда мы хотим, чтобы обновления мобильных приложений были обязательными, а иногда мы хотим выпустить обновление приложения только для определенной платформы. В нашем случае мы создаем специальное условие для платформы Android.

Затем в Firebase Remote Config мы создаем свойство с именем app_version типа JSON, которое содержит версию приложения, номер сборки и bool-флаг, является ли обновление обязательным. Чтобы разграничить значение для iOS и Android, мы используем ранее созданное условие. Сохраните, опубликуйте изменения, и все готово.

И снова мы добавляем метод в FirebaseRemoteConfigService для получения JSON-свойства app_version:

// firebase_remote_config_service.dart
class FirebaseRemoteConfigService {
  // <...>

  String getAppVersionJson() => firebaseRemoteConfig.getString('app_version');
}

Затем мы используем метод getAppVersionJson() внутри AppUpdateService:

// app_update_service.dart
import 'dart:convert';

import 'package:package_info_plus/package_info_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../firebase/firebase_remote_config_service.dart'; // add
import 'models/app_update_status.dart';
import 'models/app_version.dart';

part 'app_update_service.g.dart';

@riverpod
AppUpdateService appUpdateService(AppUpdateServiceRef ref) {
  return AppUpdateService(
    firebaseRemoteConfigService: ref.watch(firebaseRemoteConfigServiceProvider), // add
  );
}

@riverpod
Future<AppUpdateStatus> updateStatus(UpdateStatusRef ref) async {
  return ref.watch(appUpdateServiceProvider).checkForUpdate();
}

class AppUpdateService {
  const AppUpdateService({
    required this.firebaseRemoteConfigService, // add
  });

  final FirebaseRemoteConfigService firebaseRemoteConfigService; // add

  Future<AppUpdateStatus> checkForUpdate() async {
    final json = firebaseRemoteConfigService.getAppVersionJson(); // add
    final appVersion = AppVersion.fromJson( // add
      jsonDecode(json) as Map<String, dynamic>,
    );

    final packageInfo = await PackageInfo.fromPlatform();
    final currentAppVersion = AppVersion(
      version: packageInfo.version,
      buildNumber: int.tryParse(packageInfo.buildNumber) ?? 0,
    );

    return AppUpdateStatus(
      updateAvailable: currentAppVersion.compareToPreferred(appVersion),
      optional: appVersion.optional,
    );
  }
}

Создав и использовав отдельное условие для платформы Android, теперь мы можем обрабатывать поведение обновления приложения для каждой соответствующей платформы.

Это лишь один пример того, как условия могут быть применены к значениям Firebase Remote Config. Давайте посмотрим, как мы можем использовать условия, чувствительные ко времени, для реализации функции уведомления о живом потоке.

Уведомление о прямой трансляции

Чтобы определить, когда событие происходит в реальном времени, нам нужно создать условие, чувствительное ко времени. Просто создайте новое условие в консоли Firebase. Для условия выберите конкретную дату и диапазон времени, когда вы хотите, чтобы оно сработало.

Затем определите булево свойство stream_live, которое использует определенное условие. Свойство будет возвращать true только во время события, то есть когда текущие дата и время находятся в диапазоне, указанном в нашем условии.

Также добавьте еще одно свойство для ссылки на поток – stream_link, чтобы мы могли обновлять его в любое время, если потребуется. Я бы также рекомендовал группировать связанные свойства вместе для более удобного управления свойствами Firebase Remote Config.

В третий раз мы расширяем FirebaseRemoteConfigService новыми методами для получения данных потока событий:

// firebase_remote_config_service.dart
class FirebaseRemoteConfigService {
  // <...>

  String getStreamLink() => firebaseRemoteConfig.getString('stream_link');

  bool getStreamLive() => firebaseRemoteConfig.getBool('stream_live');
}

Позже мы используем методы внутри LiveStreamService:

// live_stream_service.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../firebase/firebase_remote_config_service.dart'; // add

part 'live_stream_service.g.dart';

@riverpod
LiveStreamService liveStreamService(LiveStreamServiceRef ref) {
  return LiveStreamService(
    firebaseRemoteConfigService: ref.watch(firebaseRemoteConfigServiceProvider), // add
  );
}

@riverpod
bool streamLive(StreamLiveRef ref) {
  return ref.watch(liveStreamServiceProvider).streamLive();
}

@riverpod
String streamLink(StreamLinkRef ref) {
  return ref.watch(liveStreamServiceProvider).streamLink();
}

class LiveStreamService {
  const LiveStreamService({
    required this.firebaseRemoteConfigService, // add
  });

  final FirebaseRemoteConfigService firebaseRemoteConfigService; // add

  bool streamLive() {
    return firebaseRemoteConfigService.getStreamLive(); // add
  }

  String streamLink() {
    return firebaseRemoteConfigService.getStreamLink(); // add
  }
}

Думаю, вы заметили, как мы снова и снова используем один и тот же шаблон. Добавьте новое свойство, расширьте FirebaseRemoteConfigService геттерами, а затем используйте их везде, где они необходимы. Таким образом, мы можем сохранить код последовательным и предсказуемым. Я надеюсь, что скучный код станет новым трендом в 2023 году! ????

Чтобы проверить, работает ли уведомление о событии в реальном времени, нам нужно дождаться события... или мы можем использовать консоль Firebase для изменения даты начала события ????‍♂️ Теперь, если вы откроете приложение Flutter Forward, вы должны увидеть уведомление о живом потоке в верхней части экрана.

Отлично! В следующем разделе мы рассмотрим, как можно использовать Firebase Remote Config для пометки функций и постепенного внедрения новых возможностей.

Feature flagging

Функциональный тумблер или функциональный флаг – это дистанционный переключатель, который может удаленно включить или выключить определенную функцию в приложении. Это очень полезно, когда вы запускаете новую функциональность приложения и хотите сделать это поэтапно; или вы заметили неожиданное поведение приложения и можете мгновенно отключить функцию.

Допустим, мы хотим проверить, будет ли вообще использоваться функция добавления сеанса событий в избранное. Прежде всего, мы создаем условие, которое будет применяться только к 10% нашей базы пользователей.

Затем создайте свойство переключения – favorites_enabled, которое будет включать функцию добавления в избранное в приложении и использовать ранее созданное условие.

Как обычно, расширьте FirebaseRemoteConfigService дополнительным методом:

// firebase_remote_config_service.dart
class FirebaseRemoteConfigService {
  // <...>

  bool getFavoritesEnabled() => firebaseRemoteConfig.getBool('favorites_enabled');
}

Внутри FavoritesService

  • добавьте зависимость FirebaseRemoteConfigService и

  • добавьте новое событие аналитики для отслеживания того, использовал ли пользователь функцию добавления в избранное,

и используйте значение флага функции из Firebase Remote Config:

// favorites_service.dart
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../firebase/firebase_remote_config_service.dart'; // add
import 'enums/favorite_button_type.dart';

part 'favorites_service.g.dart';

@riverpod
FavoritesService favoritesService(FavoritesServiceRef ref) {
  return FavoritesService(
    analytics: FirebaseAnalytics.instance,  // add
    firebaseRemoteConfigService: ref.watch(firebaseRemoteConfigServiceProvider),  // add
  );
}

@riverpod
bool favoritesEnabled(FavoritesEnabledRef ref) {
  return ref.watch(favoritesServiceProvider).favoritesEnabled();
}

@riverpod
FavoriteButtonType favoriteButtonType(FavoriteButtonTypeRef ref) {
  return ref.watch(favoritesServiceProvider).favoriteButtonType();
}

class FavoritesService {
  const FavoritesService({
    required this.analytics, // add
    required this.firebaseRemoteConfigService, // add
  });

  final FirebaseAnalytics analytics; // add
  final FirebaseRemoteConfigService firebaseRemoteConfigService; // add

  Future<void> addToFavorites(String id) async {
    await analytics.logEvent( // add
      name: 'add_to_favorites',
      parameters: {'session_id': id},
    ); // add
  }

  bool favoritesEnabled() {
    return firebaseRemoteConfigService.getFavoritesEnabled(); // add
  }

  FavoriteButtonType favoriteButtonType() {
    final type = firebaseRemoteConfigService.getFavoriteButtonType(); // add

    return FavoriteButtonType.fromString(type); // add
  }
}

Если проверить поведение приложения, то можно заметить, что функция добавления в избранное включена только на определенном наборе устройств, так как применяется условие "10% пользователей". Если мы достаточно уверены в новой функциональности, мы можем включить функцию глобально, удалив условие и установив значение флага в true.

Это здорово! Мы можем включать и отключать функции глобально или проводить поэтапное внедрение, включив функцию, скажем, для 10% пользователей, и постепенно увеличивая это значение. Вопрос в том, можем ли мы продвинуть эту концепцию на шаг вперед? (Спойлер: да, мы можем!)

A/B тестирование

Честно говоря, A/B тестирование очень похоже на переключение функций, только помимо изменения значений конкретного переключателя, вы также измеряете, как это конкретное значение влияет на поведение пользователя. Тогда вы сможете увидеть, какое значение работает лучше всего, чтобы быть более уверенным при внедрении новых функций для ваших пользователей.

В нашем случае мы заметили, что кнопка добавления в избранное работает плохо, и мы предполагаем, что выдвижная кнопка слишком скрыта, чтобы пользователи вообще заметили ее существование. Чтобы подтвердить это предположение, мы создадим новый переключатель favorite_button_type, который будет иметь два возможных значения: slideable и card (этот параметр будет добавлен позже с помощью A/B теста).

Теперь мы используем новое свойство внутри приложения. Во-первых, нам нужно расширить FirebaseRemoteConfigService с помощью геттера (прим.: функция здесь выполняет смысл геттера):

// firebase_remote_config_service.dart
class FirebaseRemoteConfigService {
  // <...>

    String getFavoriteButtonType() => firebaseRemoteConfig.getString('favorite_button_type');
}

Затем мы используем его внутри FavoritesService:

// favorites_service.dart
class FavoritesService {
  // <...>

  FavoriteButtonType favoriteButtonType() {
    final type = firebaseRemoteConfigService.getFavoriteButtonType();

    return FavoriteButtonType.fromString(type);
  }
}

Таким образом, мы сможем удаленно переключать тип кнопки, а еще лучше – теперь мы можем создать для этого A/B-тест.

Создание A/B теста

На приборной панели Firebase Remote Config выберите "Create your first A/B test".

Затем придумайте еще одно креативное название для эксперимента и выберите для него целевую группу.

Наша цель - увеличить конверсию кнопки добавления в избранное, поэтому мы выбираем соответствующее событие, которое мы создали ранее - add_to_favorites.

Наконец, выберите свойство Firebase Remote Config для эксперимента и определите различные значения, которые вы хотите протестировать. В нашем случае это разные типы кнопок – slideable и card .

Оставьте веса вариантов равными, чтобы это была честная борьба между разными типами кнопок.

Начните эксперимент и теперь терпеливо ждите результатов. Чтобы подтвердить результаты A/B-теста, запустим приложение на разных устройствах iOS. Вы должны заметить, что на некоторых устройствах по-прежнему используется выдвижная кнопка, а на других кнопка находится поверх карточки сеанса мероприятия – A/B-тест работает!

????‍♂️ Инфо

Помните, что для того, чтобы начать получать данные, может потребоваться до 24 часов.

Результаты A/B-тестирования

Через некоторое время вы должны увидеть результаты на приборной панели A/B-теста.

Как вы можете заметить, кнопка в виде карточки победила, опередив выдвижную кнопку на 60%. Если мы будем достаточно уверены в этих результатах, мы сможем развернуть изменения таким образом, чтобы наиболее эффективный вариант был доступен всем пользователям.

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

Выводы

Давайте вспомним, что мы узнали в этой статье:

  1. Вы можете использовать Firebase Remote Config для предоставления различных значений вашему приложению и последующего их изменения на лету.

  2. Используйте "условия" для предоставления различных значений конфигурации для ваших пользователей.

  3. Используйте Firebase Remote Config для feature flagging и постепенного внедрения новых возможностей.

  4. И не забывайте использовать A/B-тесты для подтверждения своих предположений и обеспечения наилучшего опыта для ваших пользователей.

Если вы хотите углубиться в код или попробовать приложение Flutter Forward agenda, вы можете найти полный исходный код на GitHub. В случае возникновения вопросов или предложений – обращайтесь ко мне в Twitter или по любому другому каналу социальных сетей.

Save trees. Stay SOLID. Thanks for reading.


Материал переведён Ruble.

TODO: change after

Забавная группа с неадекватным автором ☜(゚ヮ゚☜)

Комментарии (0)