FlutterアプリにRemoteConfigを導入してみた!
はじめに
Flutter アプリに Firebase
の Remote Config
を導入する手順を忘却録として書き残したいと思います。
至らない点がございましたら 、ご指摘いただければ幸いです🙇
Remote Configを導入するメリット
Remote Config
を活用することでアプリのアップデートをせずに値の変更ができるので、アプリのトップコンテンツなどで画面に表示している名称や画像を変更したいと思ったときに即座に反映することが可能です。
必要なパッケージを導入
pubspec.yaml
内 dependencies の中で、firebase_core , firebase_remote_configを追加します。
また状態管理で hooks_riverpod も追加しておきます。
ご存じの方も多いかと思われますが、Flutterでパッケージを導入する際に拡張機能のPubspec Assist
をインストールしておくと便利です。
インストール後、以下の手順でパッケージを導入できます
-
shift + command + p
でコマンドパレットを呼び出す -
Pubspec Assist:add dependencies
を指定 - 必要なパッケージを入力(スペルが曖昧でも補完機能があるので助かっています。)
コードを書く
事前準備
先に firebase 上で RemoteConfig が使える状態にしておきます。
手順は以下のとおりです。
- firebase のプロジェクト内で左のタブバーよりエンゲージメント > RemoteConfig に移動
- 「構成を作成」というボタンをクリックし、パラメータを作成する
今回は簡単に RemoteConfig の概要のようなパラメータにしようと思います。
次にflutter create後、MVVM
の設計をもとにファイル構成を行います。
先にフォルダ構成を提示しておきます。
それではコード書いて行きます!
まず Riverpod を使えるようにします。といっても簡単で、main 関数の runApp 内の widget(今回は MyApp)を ProviderScope という Widget で囲うだけです。
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'src/ui/my_app/my_app_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(ProviderScope(child: _MaterialApp()));
}
class _MaterialApp extends StatelessWidget {
MaterialApp build(BuildContext context) => const MaterialApp(
title: 'RemoteConfigPractice',
home: MyApp(),
);
}
MyApp は後で view の作成でコード載せます。
Model の作成 (resources & repository)
FlutterFire を参考に、コード上で RemoteConfig を導入するために必要な記述を行います。
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final remoteConfigServiceProvider = Provider((_) => RemoteConfigService());
class RemoteConfigService {
RemoteConfig? _remoteConfig;
Future<void> getRemoteConfig() async {
_remoteConfig = RemoteConfig.instance;
await _remoteConfig?.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(seconds: 10),
minimumFetchInterval: const Duration(hours: 1),
));//上記2行は公式のとおりに設定しております。
await _remoteConfig?.fetchAndActivate();
}
Future<RemoteConfigValue> getValue(String key) async =>
_remoteConfig!.getValue(key);
}
各メソッドについての簡単な説明です。
.getRemoteConfig
アプリが起動されたタイミングで呼び出す必要があるメソッド
・getValue
各パラーメータに分かれたRemoteConfigのデータを引っ張ってくるメソッド。引数には先程firesbase(RemoteConfig)でパラメータの作成を行った際のパラメータ名を渡します。
次にrepositoryに移ります。
repositoryはViewModelがどこからデータを取得・更新するのかを意識させないためにビジネスロジックとデータ操作を分離することを目的とします。
import 'dart:convert';
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../resources/entities/remote_content.dart';
import '../../resources/firebase/remote_config_service.dart';
final remoteContentRepositoryProvider =
Provider<RemoteContentRepository>((ref) {
final remoteConfigService = ref.read(remoteConfigServiceProvider);
return RemoteContentRepository(remoteConfigService: remoteConfigService);
});
class RemoteContentRepository {
RemoteContentRepository({
required this.remoteConfigService,
});
final RemoteConfigService remoteConfigService;
//viewModelに渡すためのメソッド
Future<List<RemoteContent>> fetchRemoteContents() async {
final data = await remoteConfigService.getValue('RemoteConfig');
// ignore: unnecessary_null_comparison
if (data == null) {
throw const HttpException(
'Failed to get Remote Config or under loading.');
}
final decoded = json.decode((data).asString()).cast<Map<String, dynamic>>()
as List<Map<String, dynamic>>;
final remoteContent = await Future.wait(decoded.map(
(content) async => RemoteContent.fromJson(content),
));
// ignore: unnecessary_null_comparison
remoteContent.removeWhere((content) => content == null);
return remoteContent;
}
}
ここで記載されているRemoteContent
とありますが、先程JSON エディタ上で記述した内容をview で呼び出す際タイポしないようにモデルクラスを定義しています。
別ファイルで定義しています。
title や content は先程 firebase(RemoteConfig)上で編集したものです。
class RemoteContent {
RemoteContent({
required this.title,
required this.content,
});
factory RemoteContent.fromJson(Map<String, dynamic> data) => RemoteContent(
title: data['title'],
content: data['content'],
);
final String title;
final String content;
}
Modelの記述は以上で、次にviewModelに移ります。
viewModel の作成
状態管理が必要な場合、viewModel は1つの view に対し1つ用意するという方針なので、今回は my_app フォルダに view_Model ファイルを用意します。
viewModel の役割は大きく2つあります。
- View から入力された状態(データ)を適切に変換して Model として持つ
- Model の状態(データ)を View 渡して画面の更新を促す
今回はまさに2点目の Model(RemoteConfig) のデータ(パラメータ)を view に渡して画面の更新を促す ということをしたいです。
コードは以下のとおりです。
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../repository/remote_content_repository.dart';
import '../../resources/entities/remote_content.dart';
import '../../resources/firebase/remote_config_service.dart';
final myAppPageViewModelProvider = ChangeNotifierProvider.autoDispose((ref) {
final remoteConfigService = ref.read(remoteConfigServiceProvider);
final remoteContentRepository = ref.read(remoteContentRepositoryProvider);
return _MyAppPageViewModel(
remoteConfigService: remoteConfigService,
remoteContentRepository: remoteContentRepository,
);
});
class _MyAppPageViewModel extends ChangeNotifier {
_MyAppPageViewModel({
required this.remoteConfigService,
required this.remoteContentRepository,
}) {
_initialize();
}
final RemoteConfigService remoteConfigService;
final RemoteContentRepository remoteContentRepository;
AsyncValue<List<RemoteContent>> _remoteContents = const AsyncValue.loading();
AsyncValue<List<RemoteContent>> get remoteContents => _remoteContents;
bool _isLoading = true;
bool get isLoading => _isLoading;
Future<void> _initialize() async {
try {
await remoteConfigService.getRemoteConfig();
_isLoading = false;
final remoteContents =
await remoteContentRepository.fetchRemoteContents();
_remoteContents = AsyncValue.data(remoteContents);
notifyListeners();
} on Exception catch (e) {
print(e);
_remoteContents = AsyncValue.error(e);
}
}
}
view の記述
最後に view(my_app_page) です。
AsyncValue(今回でいうと remoteContents)についての説明は以下の記事が参考になります。
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../common/error_page.dart';
import '../../common/loading_page.dart';
import '../../common_style.dart';
import 'view_model.dart';
class MyApp extends HookConsumerWidget {
const MyApp();
Widget build(BuildContext context, WidgetRef ref) {
final viewModel = ref.watch(myAppPageViewModelProvider);
final isLoading = viewModel.isLoading;
if (isLoading) {
return const LoadingPage();
}
return Scaffold(
body: viewModel.remoteContents.when(
loading: () => const LoadingPage(),
error: (_, __) => const ErrorPage(),
data: (contents) => SafeArea(
child: ListView.builder(
itemCount: contents.length,
itemBuilder: (context, index) {
final content = contents[index];
return Container(
margin: const EdgeInsets.symmetric(vertical: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.fromLTRB(15, 0, 15, 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
content.title.isEmpty
? const SizedBox()
: Text(
content.title,
style: CommonTextStyles.remoteContentTitle,
),
const SizedBox(height: 10),
content.content.isEmpty
? const SizedBox()
: Text(
content.content,
style: CommonTextStyles.remoteContent,
),
],
),
),
],
),
);
},
),
),
),
);
}
}
以上のコードより RemoteConfig からのデータを画面上に反映されました!
以上になります。ここまで読んでいただきありがとうございました!!
Discussion
Riverpod公式でChangeNotifierProviderは非推奨なので使用しないほうが良さそうです。
この場面ではFeatureProviderを使うのがよさそうです。