diff --git a/.gitignore b/.gitignore index 29a3a50..99c5328 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,11 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Firebase +.firebase +.firebaserc +firebase.json +firebase_options.dart +google-services.json +GoogleService-info.plist \ No newline at end of file diff --git a/README.md b/README.md index 58c47da..0e2c1ef 100644 --- a/README.md +++ b/README.md @@ -11,37 +11,333 @@ Demo of firebase #### 1.1 Firebase tools - 安裝指令 - ```shell - npm install -g firebase-tools - ``` + ```shell + npm install -g firebase-tools + ``` - 如果版本不符,會看到類似如下截圖 + ![update npm version](docs/npm-version-notice.png) #### 1.2 FlutterFire CLI - 安裝指令 - ```shell - dart pub global activate flutterfire_cli - ``` + ```shell + dart pub global activate flutterfire_cli + ``` ### 2. 使用 #### 2.1 登入 Firebase - 指令 - ```shell - firebase login - ``` + ```shell + firebase login + ``` - 登入可能有兩種狀況 - 登入方式一:可以在本地端直接以瀏覽器同步登入 - ![open browser](docs/firebase-login-browser.png) + - 開啟瀏覽器 + + ![open browser](docs/firebase-login-browser.png) + + - 登入「成功」 + + ![login_successful](docs/firebase-login-browser-successful.png) + - 登入方式二:透過連結的非同步跨環境登入 - Terminal 給的連結 + ![open browser](docs/firebase-login-link-1.png) + - 登入 Google 帳號,確認是同樣的 session ID + ![open browser](docs/firebase-login-link-2-session-id.png) + - 複製代碼貼回 Terminal + ![open browser](docs/firebase-login-link-3-code.png) - -### 3. \ No newline at end of file + +#### 2.2 設定 Flutter 專案 + +###### A. 專案環境更新 + +1. 設定 對應的 Firebase 專案 + + - 指令 + ```shell + flutterfire configure + ``` + - 可以先觀察一下專案中加入哪些檔案 + +2. 在 Flutter 專案中加入 `firebase_core` + - 指令 + ```shell + flutter pub add firebase_core + ``` + +###### B. `main.dart` + +1. 在 main function 中加入以下程式碼 + ```dart + Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + runApp(const MyApp()); + } + ``` +2. 新增 import + + ```dart + import 'package:firebase_core/firebase_core.dart'; + import 'package:firebase_database_demo/firebase_options.dart'; + ``` + +### 3 使用 Firebase 的功能 + +#### 3.1 使用 Hosting + +1. 指令 + ```shell + firebase experiments:enable webframeworks + firebase init hosting + ``` + - 指令執行下去後,會依序看到下方幾個操作步驟 + - 選擇要使用 hosting 的專案 + - 會問是否使用 Flutter Web codebase,預設:Yes + - 選擇 server 的地區 + - 是否使用自動部署,預設:No + - 可以觀察一下專案中增加的東西 + +2. 部署 + + ```shell + firebase deploy + ``` + +#### 3.2 使用 RemoteConfig + +##### 3.2.1 Firebase 的設定 + +1. 先到 Firebase Project 中增加一個 `version` 的字串設定值 + +##### 3.2.2 Flutter 的程式碼變動 + +###### A. 專案環境更新 + +1. 在 Flutter 專案中加入 `firebase_remote_config` + - 指令 + ```shell + flutter pub add firebase_remote_config + ``` + +###### B. `main.dart` + +1. 增加一個全域變數 `remoteConfig` + ```dart + // 在 main function 上面加入全域變數 + late FirebaseRemoteConfig remoteConfig; + + Future main() async { + //... + } + ``` + +2. 加入 remote config 的初式化函式 + ```dart + Future main() async { + //... + } + + // 加在 main function 下方 + _initialRemoteConfig() async { + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(minutes: 1), + minimumFetchInterval: const Duration(hours: 1), + )); + await remoteConfig.setDefaults(const { + "version": "0.1.0", + }); + await remoteConfig.fetchAndActivate(); + } + ``` + +3. 於 main() 中初始化 + ```dart + Future main() async { + //... + remoteConfig = FirebaseRemoteConfig.instance; + await _initialRemoteConfig(); + runApp(const MyApp()); + } + + _initialRemoteConfig() async { + //... + } + ``` + +###### C. `home_page.dart` + +1. 在 _HomePageState 中,加入變數呼叫 + ```dart + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _version = remoteConfig.getString("version"); + }); + }); + ``` + +#### 3.3 使用 Realtime Database + +#### 3.3.1 Firebase 的設定 + +1. 先到 Firebase Project 中增加一個 `counter` 的數值 +2. 先確認規則(rule),測試開發階段先開啟,日後要記得調回來 + + ```json + { + /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ + "rules": { + ".read": true, + ".write": true + } + } + ``` + +#### 3.3.2 Flutter 的程式碼變動 + +###### A. 專案環境更新 + +1. 設定完畢後,回到 Flutter 專案來執行 firebase_opetions 更新 + - 指令 + ```shell + flutterfire configure + ``` +2. 為專案加入 `firebase_database` + - 指令 + ```shell + flutter pub add firebase_database + ``` + +###### B. 程式碼變動:`home_page.dart` + +1. 取出資料 + - 加入 `_getCounterFromRealtimeDatabase` + ```dart + _getCounterFromRealtimeDatabase() async { + DatabaseReference ref = FirebaseDatabase.instance.ref(); + final snapshot = await ref.child("counter").get(); + if (snapshot.exists) { + setState(() { + _counter = snapshot.value as int; + }); + } + } + ``` + - 於 initState() 中加入呼叫 + ```dart + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _version = remoteConfig.getString("version"); + }); + // 加入呼叫 + _getCounterFromRealtimeDatabase(); + }); + ``` +2. 更新資料 + - 在 _increaseCounter() 中加入下方程式碼 + ```dart + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _version = remoteConfig.getString("version"); + }); + // 加入更新 + DatabaseReference ref = FirebaseDatabase.instance.ref(); + ref.update({"counter": _counter}); + }); + ``` + +#### 3.4 使用 Firestore + +#### 3.4.1 Firebase 的設定 + +1. 建立資料庫 + - 「位置」選擇離服務當地最近者。 + - 要注意設定後無法變更。 +2. 注意一下「規則」 +3. 新增資料集合 + - 集合 ID:`news` + - 文件 ID:用序號 + - 欄位:`title` + - 欄位:`content` + - 資料就麻煩各位自行隨便找一下新聞的標題跟內容嘍 + +#### 3.4.2 Flutter 的程式碼變動 + +###### A. 專案環境更新 + +- 為專案加入 `cloud_firestore` + - 指令 + ```shell + flutter pub add cloud_firestore + ``` + +###### B. 程式碼變動:`news_page.dart` + +1. 宣告並初始化 FirebaseFirestore + ```dart + final FirebaseFirestore _db = FirebaseFirestore.instance; + ``` +2. 取得資料,修改 _initialNewsList + - 將原本迴圈的程式碼改成 + ```dart + final collectionRef = _db.collection("news"); + collectionRef.get().then((querySnapshot) { + for (QueryDocumentSnapshot docSnapshot in querySnapshot.docs) { + Map snapshot = docSnapshot.data() as Map; + final News news = News(title: snapshot["title"] ?? "", context: snapshot["content"]); + setState(() { + _list.add(news); + }); + } + }); + ``` +3. 新增資料,修改 _createPost + ```dart + _createPost() { + // 新增一筆資料 + if (_titleController.text.isEmpty && _contentController.text.isEmpty) { + return; + } + final News news = News(title: _titleController.text, context: _contentController.text); + final collectionRef = _db.collection("news"); + int id = _list.length + 1; + collectionRef.doc(id.toString()).set({"title": news.title, "content": news.context}).then((value) { + setState(() { + _list.insert(0, news); + }); + }).catchError((error) { + print(error.toString()); + }); + } + ``` +4. 排序 + - Firestore 的階層為 collection > document > 文件中的欄位 + - 因此需指定 + - .orderBy("欄位名稱") + - 如果要反序:.orderBy("欄位名稱", descending: true) + - 部份程式碼的完整範例如下 + ```dart + final collectionRef = _db.collection("news"); + collectionRef.orderBy("title", descending: true).get().then((querySnapshot) { + for (QueryDocumentSnapshot docSnapshot in querySnapshot.docs) { + Map snapshot = docSnapshot.data() as Map; + final News news = News(title: snapshot["title"] ?? "", context: snapshot["content"]); + setState(() { + _list.add(news); + }); + } + }); + ``` \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 0a2bc5b..0852c37 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,8 @@ plugins { id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" diff --git a/android/settings.gradle b/android/settings.gradle index b9e43bd..9759a22 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,6 +19,9 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.1.0" apply false + // START: FlutterFire Configuration + id "com.google.gms.google-services" version "4.3.15" apply false + // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "1.8.22" apply false } diff --git a/docs/firebase-login-browser-successful.png b/docs/firebase-login-browser-successful.png new file mode 100644 index 0000000..6bf1294 Binary files /dev/null and b/docs/firebase-login-browser-successful.png differ diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/home_page.dart b/lib/home_page.dart index 4cb0adc..726f18f 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -1,3 +1,6 @@ +import 'package:firebase_database/firebase_database.dart'; +import 'package:firebase_database_demo/main.dart'; +import 'package:firebase_database_demo/news_page.dart'; import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { @@ -8,10 +11,31 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - final String _version = "0.0.1"; + String _version = "0.0.1"; int _counter = 0; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _version = remoteConfig.getString("version"); + }); + _getCounterFromRealtimeDatabase(); + }); + } + + _getCounterFromRealtimeDatabase() async { + DatabaseReference ref = FirebaseDatabase.instance.ref(); + final snapshot = await ref.child("counter").get(); + if (snapshot.exists) { + setState(() { + _counter = snapshot.value as int; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -23,6 +47,19 @@ class _HomePageState extends State { "Flutter x Firebase", style: TextStyle(fontSize: 48.0, fontWeight: FontWeight.bold), ), + ElevatedButton( + onPressed: () { + // 進入 news_page.dart + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const NewsPage()), + ); + }, + child: const Text("進入"), + ), + const SizedBox( + height: 8, + ), Wrap( children: [ Text("版本: $_version"), @@ -39,7 +76,7 @@ class _HomePageState extends State { onPressed: () { _increaseCounter(); }, - child: Text("自主\n增加"), + child: const Text("自主\n增加"), ), ); } @@ -48,5 +85,7 @@ class _HomePageState extends State { setState(() { _counter++; }); + DatabaseReference ref = FirebaseDatabase.instance.ref(); + ref.update({"counter": _counter}); } } diff --git a/lib/main.dart b/lib/main.dart index c4db010..c1c3542 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,34 @@ -import 'package:firebase_database/home_page.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_database_demo/firebase_options.dart'; +import 'package:firebase_database_demo/home_page.dart'; +import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; -void main() { +late FirebaseRemoteConfig remoteConfig; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + remoteConfig = FirebaseRemoteConfig.instance; + await _initialRemoteConfig(); + runApp(const MyApp()); } +_initialRemoteConfig() async { + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(minutes: 1), + minimumFetchInterval: const Duration(hours: 1), + )); + await remoteConfig.setDefaults(const { + "version": "0.1.0", + }); + await remoteConfig.fetchAndActivate(); +} + class MyApp extends StatelessWidget { const MyApp({super.key}); diff --git a/lib/news.dart b/lib/news.dart new file mode 100644 index 0000000..1e5f767 --- /dev/null +++ b/lib/news.dart @@ -0,0 +1,9 @@ +class News { + String title; + String context; + + News({ + required this.title, + required this.context, + }); +} diff --git a/lib/news_page.dart b/lib/news_page.dart new file mode 100644 index 0000000..0dd3ee8 --- /dev/null +++ b/lib/news_page.dart @@ -0,0 +1,113 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_database_demo/news.dart'; +import 'package:flutter/material.dart'; + +class NewsPage extends StatefulWidget { + const NewsPage({super.key}); + + @override + State createState() => _NewsPageState(); +} + +class _NewsPageState extends State { + final TextEditingController _titleController = TextEditingController(); + final TextEditingController _contentController = TextEditingController(); + + final FirebaseFirestore _db = FirebaseFirestore.instance; + List _list = []; + + @override + void initState() { + _initialNewsList(); + super.initState(); + } + + _initialNewsList() { + _list = []; + + final collectionRef = _db.collection("news"); + collectionRef.orderBy("title", descending: true).get().then((querySnapshot) { + for (QueryDocumentSnapshot docSnapshot in querySnapshot.docs) { + Map snapshot = docSnapshot.data() as Map; + final News news = News(title: snapshot["title"] ?? "", context: snapshot["content"]); + setState(() { + _list.add(news); + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: ListTile( + title: Column( + children: [ + TextFormField( + controller: _titleController, + decoration: const InputDecoration(hintText: "標題"), + ), + TextFormField( + controller: _contentController, + decoration: const InputDecoration(hintText: "內容"), + ), + ], + ), + trailing: ElevatedButton( + onPressed: () { + _createPost(); + }, + child: const Text("新增"), + ), + ), + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text("新聞列表"), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: ListView.separated( + itemBuilder: (context, index) { + return ListTile( + title: Text(_list[index].title), + subtitle: Text(_list[index].context), + ); + }, + separatorBuilder: (context, index) { + return const Divider( + height: 1, + ); + }, + itemCount: _list.length, + ), + ), + ), + ], + ), + ); + } + + _createPost() { + // 新增一筆資料 + if (_titleController.text.isEmpty && _contentController.text.isEmpty) { + return; + } + final News news = News(title: _titleController.text, context: _contentController.text); + final collectionRef = _db.collection("news"); + int id = _list.length + 1; + collectionRef.doc(id.toString()).set({"title": news.title, "content": news.context}).then((value) { + setState(() { + _list.insert(0, news); + }); + }).catchError((error) { + print(error.toString()); + }); + } +} diff --git a/pubspec.lock b/pubspec.lock index b9fd799..452cf3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "71c01c1998c40b3af1944ad0a5f374b4e6fef7f3d2df487f3970dbeadaeb25a1" + url: "https://pub.dev" + source: hosted + version: "1.3.46" async: dependency: transitive description: @@ -33,6 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: d5b73c5f27d95504e45d96e793fe9f9daa62e42b22da85b9f8de4916f46e916d + url: "https://pub.dev" + source: hosted + version: "5.5.0" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "8d5a3a501f3b21638199819ab76709d3b87abc9a565ed611b13a238611cfba27" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "24f302e4ffd88ec2891e33f432b53f59ebd072664c5dc804b7fcbc51ea22b4b3" + url: "https://pub.dev" + source: hosted + version: "4.3.4" collection: dependency: transitive description: @@ -57,6 +89,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "2438a75ad803e818ad3bd5df49137ee619c46b6fc7101f4dbc23da07305ce553" + url: "https://pub.dev" + source: hosted + version: "3.8.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 + url: "https://pub.dev" + source: hosted + version: "5.3.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 + url: "https://pub.dev" + source: hosted + version: "2.18.1" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: de7474c0d412fbeedb68eaeced2ef2a60de8673b3fb05bea50737c6f088ce3f4 + url: "https://pub.dev" + source: hosted + version: "11.1.6" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: "0fce969fa8f7dc00c2b1593e925a15435c4018451fa68c34f49d7e15876e9ff2" + url: "https://pub.dev" + source: hosted + version: "0.2.5+46" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: "99da90669d271c4123ab6b9d2b322f2dcf2bdd11b10c9c363befc528b2f8e8b7" + url: "https://pub.dev" + source: hosted + version: "0.2.6+4" + firebase_remote_config: + dependency: "direct main" + description: + name: firebase_remote_config + sha256: "29619302396823622dd9d0ccb8ac2b48804c518c0c82aa85ef3d86b91fdf55ca" + url: "https://pub.dev" + source: hosted + version: "5.1.5" + firebase_remote_config_platform_interface: + dependency: transitive + description: + name: firebase_remote_config_platform_interface + sha256: ae899364a77fdf706c2ec37f1336df1dd3cc6777f8cd6ce7cf0cae65830bf1cf + url: "https://pub.dev" + source: hosted + version: "1.4.46" + firebase_remote_config_web: + dependency: transitive + description: + name: firebase_remote_config_web + sha256: "6f018a0d7ce4b67784d29ec7193806e494adc243a4c880a4fab49e59afca212f" + url: "https://pub.dev" + source: hosted + version: "1.7.4" flutter: dependency: "direct main" description: flutter @@ -75,6 +179,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -139,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -208,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.5.3 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8e86689..8df4740 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: firebase_database +name: firebase_database_demo description: "Demo of firebase" publish_to: 'none' # Remove this line if you wish to publish to pub.dev @@ -14,6 +14,10 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.8 + firebase_core: ^3.8.0 + firebase_remote_config: ^5.1.5 + firebase_database: ^11.1.6 + cloud_firestore: ^5.5.0 dev_dependencies: flutter_test: