Kenali Firebase untuk Flutter

1. Sebelum Anda mulai

Dalam codelab ini, Anda akan belajar beberapa dasar-dasar Firebase untuk membuat Flutter aplikasi mobile untuk Android dan iOS.

Prasyarat

Codelab ini mengasumsikan Anda sudah familiar dengan Flutter, dan Anda telah menginstal Flutter SDK , dan editor .

Apa yang akan Anda buat

Dalam codelab ini Anda akan membuat aplikasi obrolan RSVP dan buku tamu acara di Android, iOS, Web, dan macOS menggunakan Flutter. Anda akan mengautentikasi pengguna dengan Firebase Authentication dan menyinkronkan data menggunakan Cloud Firestore.

Apa yang Anda butuhkan?

Anda dapat menjalankan codelab ini menggunakan salah satu perangkat berikut:

  • Perangkat fisik (Android atau iOS) yang terhubung ke komputer Anda dan disetel ke mode pengembang.
  • Simulator iOS. (Membutuhkan memasang alat Xcode .)
  • Emulator Androidnya. (Membutuhkan setup pada Android Studio .)

Selain yang di atas, Anda juga membutuhkan:

  • Peramban pilihan Anda, seperti Chrome.
  • IDE atau editor teks pilihan Anda, seperti Android Studio atau Kode VS dikonfigurasi dengan Dart dan Flutter plugin.
  • Terbaru stable versi Flutter (atau beta jika Anda menikmati hidup di tepi).
  • Akun Google, seperti akun gmail, untuk membuat dan mengelola proyek Firebase Anda.
  • The firebase tool baris perintah , login ke akun gmail Anda.
  • Kode sampel codelab. Lihat langkah selanjutnya untuk cara mendapatkan kodenya.

2. Dapatkan kode sampel

Mari kita mulai dengan mengunduh versi awal proyek kita dari GitHub.

Mengkloning repositori GitHub dari baris perintah:

git clone https://github.com/flutter/codelabs.git flutter-codelabs

Atau, jika Anda memiliki cli GitHub ini alat diinstal:

gh repo clone flutter/codelabs flutter-codelabs

Kode contoh harus kloning ke dalam flutter-codelabs direktori, yang berisi kode untuk koleksi codelabs. Kode untuk codelab ini di flutter-codelabs/firebase-get-to-know-flutter .

Struktur direktori di bawah flutter-codelabs/firebase-get-to-know-flutter adalah serangkaian snapshot dari mana Anda harus pada akhir setiap langkah yang bernama. Ini adalah Langkah 2, jadi menemukan file yang cocok semudah:

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Jika Anda ingin melompat ke depan, atau melihat seperti apa tampilan setelah langkah, lihat di direktori yang dinamai sesuai langkah yang Anda minati.

Impor aplikasi pemula

Terbuka atau mengimpor flutter-codelabs/firebase-get-to-know-flutter/step_02 direktori ke IDE pilihan Anda. Direktori ini berisi kode awal untuk codelab yang terdiri dari aplikasi pertemuan Flutter yang belum berfungsi.

Temukan file untuk dikerjakan

Kode dalam aplikasi ini tersebar di beberapa direktori. Pemisahan fungsi ini dirancang untuk mempermudah pengerjaan, dengan mengelompokkan kode berdasarkan fungsionalitas.

Temukan file berikut dalam proyek:

  • lib/main.dart : File ini berisi titik masuk utama dan widget aplikasi.
  • lib/src/widgets.dart : File ini berisi beberapa widget untuk membantu standarisasi styling aplikasi. Ini digunakan untuk membuat layar aplikasi pemula.
  • lib/src/authentication.dart : File ini berisi implementasi parsial FirebaseUI Tupoksi dengan satu set widget untuk membuat login pengalaman pengguna untuk otentikasi berbasis Firebase email. Widget untuk alur autentikasi ini belum digunakan di aplikasi pemula, tetapi Anda akan segera menghubungkannya.

Anda akan menambahkan file tambahan yang diperlukan untuk membangun sisa aplikasi.

Meninjau lib/main.dart berkas

Aplikasi ini mengambil keuntungan dari google_fonts paket untuk memungkinkan kita untuk membuat Roboto font default di seluruh aplikasi. Latihan bagi pembaca termotivasi adalah untuk mengeksplorasi fonts.google.com dan menggunakan font Anda menemukan ada di berbagai bagian aplikasi.

Anda memanfaatkan widget penolong dari lib/src/widgets.dart dalam bentuk Header , Paragraph dan IconAndDetail . Widget ini mengurangi kekacauan dalam tata letak halaman yang dijelaskan dalam HomePage dengan menghilangkan duplikasi kode. Ini memiliki manfaat tambahan untuk memungkinkan tampilan dan nuansa yang konsisten.

Berikut tampilan aplikasi Anda di Android, iOS, Web, dan macOS:

Pratinjau aplikasi

3. Buat dan siapkan proyek Firebase

Menampilkan informasi acara sangat bagus untuk tamu Anda, tetapi hanya menampilkan acara tidak terlalu berguna bagi siapa pun. Mari tambahkan beberapa fungsi dinamis ke aplikasi ini. Untuk ini, Anda harus menghubungkan Firebase ke aplikasi Anda. Untuk memulai Firebase, Anda harus membuat dan menyiapkan proyek Firebase.

Buat proyek Firebase

  1. Masuk ke Firebase .
  2. Di konsol Firebase, klik Add Project (atau Buat proyek), dan nama Anda Firebase proyek Firebase-Flutter-Codelab.

4395e4e67c08043a.png

  1. Klik melalui opsi pembuatan proyek. Terima persyaratan Firebase jika diminta. Lewati penyiapan Google Analytics, karena Anda tidak akan menggunakan Analytics untuk aplikasi ini.

b7138cde5f2c7b61.png

Untuk mempelajari lebih lanjut tentang proyek Firebase, lihat Memahami proyek Firebase .

Aplikasi yang Anda buat menggunakan beberapa produk Firebase yang tersedia untuk aplikasi web:

  • Firebase Otentikasi untuk memungkinkan pengguna untuk masuk ke aplikasi Anda.
  • Cloud Firestore untuk menyimpan data terstruktur di atas awan dan mendapatkan pemberitahuan instan ketika data perubahan.
  • Firebase Aturan Keamanan untuk mengamankan database Anda.

Beberapa produk ini memerlukan konfigurasi khusus atau perlu diaktifkan menggunakan Firebase console.

Aktifkan email masuk untuk Firebase Authentication

Untuk memungkinkan pengguna untuk masuk ke aplikasi web, Anda akan menggunakan Email / Password masuk metode untuk codelab ini:

  1. Dalam Firebase konsol, memperluas menu Build di panel sebelah kiri.
  2. Klik Authentication, dan kemudian klik Dapatkan Started tombol, maka Sign-in metode tab (atau klik disini untuk langsung ke Sign-in tab metode).
  3. Klik Email / Password di Sign-in penyedia daftar, mengatur Aktifkan saklar ke posisi, dan kemudian klik Simpan. 58e3e3e23c2f16a4.png

Aktifkan Cloud Firestore

Aplikasi web menggunakan Cloud Firestore untuk menyimpan pesan chat dan menerima pesan chat baru.

Aktifkan Cloud Firestore:

  1. Pada bagian Build Firebase konsol, klik Cloud Firestore.
  2. Klik Membuat database. 99e8429832d23fa3.png
  1. Pilih Mulai dalam pilihan mode uji. Baca penafian tentang aturan keamanan. Mode uji memastikan bahwa Anda dapat dengan bebas menulis ke database selama pengembangan. Klik Next. 6be00e26c72ea032.png
  1. Pilih lokasi untuk database Anda (Anda bisa menggunakan default). Perhatikan bahwa lokasi ini tidak dapat diubah nanti. 278656eefcfb0216.png
  2. Klik Aktifkan.

4. Konfigurasi Firebase

Untuk menggunakan Firebase dengan Flutter, Anda harus mengikuti proses untuk mengonfigurasi proyek Flutter agar dapat menggunakan library FlutterFire dengan benar:

  • Tambahkan dependensi FlutterFire ke proyek Anda
  • Daftarkan platform yang diinginkan pada proyek Firebase
  • Unduh file konfigurasi khusus platform, dan tambahkan ke kode.

Dalam direktori tingkat atas dari aplikasi Flutter Anda, ada subdirektori disebut android , ios , macos dan web . Direktori ini masing-masing menyimpan file konfigurasi khusus platform untuk iOS dan Android.

Konfigurasikan dependensi

Anda perlu menambahkan pustaka FlutterFire untuk dua produk Firebase yang Anda gunakan di aplikasi ini - Firebase Auth dan Cloud Firestore. Jalankan tiga perintah berikut untuk menambahkan dependensi.

$ flutter pub add firebase_core 
Resolving dependencies...
+ firebase_core 1.10.5
+ firebase_core_platform_interface 4.2.2
+ firebase_core_web 1.5.2
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  test_api 0.4.3 (0.4.8 available)
Changed 5 dependencies!

The firebase_core adalah kode umum diperlukan untuk semua plugin Firebase Flutter.

$ flutter pub add firebase_auth
Resolving dependencies...
+ firebase_auth 3.3.3
+ firebase_auth_platform_interface 6.1.8
+ firebase_auth_web 3.3.4
+ intl 0.17.0
  test_api 0.4.3 (0.4.8 available)
Changed 4 dependencies!

The firebase_auth memungkinkan integrasi dengan kemampuan Authentication Firebase ini.

$ flutter pub add cloud_firestore
Resolving dependencies...
+ cloud_firestore 3.1.4
+ cloud_firestore_platform_interface 5.4.9
+ cloud_firestore_web 2.6.4
  test_api 0.4.3 (0.4.8 available)
Changed 3 dependencies!

The cloud_firestore memungkinkan akses ke penyimpanan data Cloud Firestore.

$ flutter pub add provider
Resolving dependencies...
+ nested 1.0.0
+ provider 6.0.1
  test_api 0.4.3 (0.4.8 available)
Changed 2 dependencies!

Meskipun Anda telah menambahkan paket yang diperlukan, Anda juga perlu mengonfigurasi proyek iOS, Android, macOS, dan Web runner untuk menggunakan Firebase dengan tepat. Anda juga menggunakan provider paket yang akan memungkinkan pemisahan logika bisnis dari logika layar.

Instalasi flutterfire

  1. FlutterFire CLI bergantung pada Firebase CLI yang mendasarinya. Jika Anda belum melakukannya, menginstal atau update ke versi terbaru dari Firebase CLI .
  2. Berikutnya, instal FlutterFire CLI dengan menjalankan perintah berikut:
    $ dart pub global activate flutterfire_cli
    

Setelah instalasi, flutterfire perintah akan tersedia secara global.

Mengonfigurasi aplikasi Anda

CLI mengekstrak informasi dari proyek Firebase Anda dan aplikasi proyek yang dipilih untuk menghasilkan semua konfigurasi untuk platform tertentu.

Di root aplikasi Anda, jalankan perintah configure:

$ flutterfire configure

Perintah konfigurasi akan memandu Anda melalui sejumlah proses:

  1. Memilih proyek Firebase (berdasarkan file .firebaserc atau dari Firebase Console).
  2. Konfirmasi platform apa (misalnya Android, iOS, macOS & web) yang Anda inginkan untuk konfigurasi.
  3. Identifikasi aplikasi Firebase untuk platform yang dipilih yang harus digunakan untuk mengekstrak konfigurasi. Secara default, CLI akan mencoba mencocokkan aplikasi Firebase secara otomatis berdasarkan konfigurasi proyek Anda saat ini.
  4. Buat file firebase_options.dart di proyek Anda.

Konfigurasikan macOS

Flutter di macOS membangun aplikasi yang sepenuhnya terkotak pasir. Karena aplikasi ini berintegrasi menggunakan jaringan untuk berkomunikasi dengan server Firebase, Anda perlu mengonfigurasi aplikasi Anda dengan hak istimewa klien jaringan.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

Lihat Hak-hak dan App Sandbox untuk lebih detail.

5. Tambahkan login pengguna (RSVP)

Sekarang bahwa Anda telah menambahkan Firebase untuk aplikasi, Anda dapat mengatur tombol RSVP yang register orang yang menggunakan Firebase Authentication . Untuk bawaan Android, bawaan iOS, dan Web ada paket FirebaseUI Auth bawaan, tetapi untuk Flutter Anda perlu membangun kemampuan ini.

Proyek yang Anda ambil di Langkah 2 menyertakan satu set widget yang mengimplementasikan antarmuka pengguna untuk sebagian besar alur autentikasi. Anda akan menerapkan logika bisnis untuk mengintegrasikan Firebase Authentication ke dalam aplikasi.

Logika Bisnis dengan Penyedia

Anda akan menggunakan provider paket untuk membuat aplikasi terpusat objek negara tersedia di seluruh pohon aplikasi widget Flutter. Untuk mulai dengan, memodifikasi impor di bagian atas lib/main.dart :

lib/main.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';           // new

import 'firebase_options.dart';                    // new
import 'src/authentication.dart';                  // new
import 'src/widgets.dart';

The import garis memperkenalkan Firebase Core dan Tupoksi, tarik dalam provider paket yang Anda gunakan untuk membuat aplikasi objek negara yang tersedia melalui pohon widget, dan termasuk widget otentikasi dari lib/src .

Ini objek negara aplikasi, ApplicationState , memiliki dua tanggung jawab utama untuk langkah ini, tetapi akan mendapatkan tanggung jawab tambahan ketika Anda menambahkan kemampuan lebih untuk aplikasi dalam langkah-langkah selanjutnya. Tanggung jawab pertama adalah untuk menginisialisasi perpustakaan Firebase dengan panggilan untuk Firebase.initializeApp() , dan kemudian ada penanganan aliran otorisasi. Menambahkan kelas berikut untuk akhir lib/main.dart :

lib/main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

Perlu dicatat beberapa poin penting dalam kelas ini. Pengguna memulai tanpa otentikasi, aplikasi menunjukkan formulir yang meminta alamat email pengguna, tergantung pada apakah alamat email itu ada di file, aplikasi akan meminta pengguna mendaftar, atau meminta kata sandi mereka, dan kemudian mengasumsikan semuanya berhasil, pengguna diautentikasi.

Harus diperhatikan bahwa ini bukan implementasi lengkap dari alur FirebaseUI Auth, karena tidak menangani kasus pengguna dengan akun yang ada yang mengalami masalah saat masuk. Menerapkan kemampuan tambahan ini dibiarkan sebagai latihan untuk pembaca termotivasi.

Mengintegrasikan alur Otentikasi

Sekarang bahwa Anda memiliki awal negara aplikasi saatnya untuk kawat negara aplikasi ke dalam inisialisasi aplikasi dan menambahkan aliran otentikasi ke HomePage . Update titik masuk utama untuk mengintegrasikan negara aplikasi melalui provider paket:

lib/main.dart

void main() {
  // Modify from here
  runApp(
    ChangeNotifierProvider(
      create: (context) => ApplicationState(),
      builder: (context, _) => App(),
    ),
  );
  // to here.
}

Modifikasi ke main fungsi membuat paket penyedia bertanggung jawab untuk instantiating objek negara aplikasi menggunakan ChangeNotifierProvider widget. Anda menggunakan ini kelas penyedia tertentu karena objek negara aplikasi meluas ChangeNotifier dan ini memungkinkan provider paket untuk tahu kapan untuk menampilkan kembali tergantung widget. Akhirnya, mengintegrasikan negara aplikasi dengan Authentication dengan memperbarui HomePage 's build metode:

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

Anda instantiate Authentication widget, dan bungkus dalam Consumer widget. Konsumen widget cara yang biasa bahwa provider paket dapat digunakan untuk membangun kembali bagian dari pohon ketika perubahan negara aplikasi. The Authentication widget adalah UI otentikasi bahwa Anda sekarang akan tes.

Menguji alur Otentikasi

cdf2d25e436bd48d.png

Berikut adalah awal dari alur otentikasi, di mana pengguna dapat menekan tombol RSVP, untuk memulai formulir email.

2a2cd6d69d172369.png

Setelah memasukkan email, sistem mengkonfirmasi jika pengguna sudah terdaftar, dalam hal ini pengguna dimintai kata sandi, atau jika pengguna tidak terdaftar, maka mereka pergi melalui formulir pendaftaran.

e5e65065dba36b54.png

Pastikan untuk mencoba memasukkan kata sandi pendek (kurang dari enam karakter) untuk memeriksa alur penanganan kesalahan. Jika pengguna terdaftar, mereka akan melihat kata sandi untuknya.

fbb3ea35fb4f67a.png

Pada halaman ini pastikan untuk memasukkan password yang salah untuk memeriksa penanganan error pada halaman ini. Terakhir, setelah pengguna masuk, Anda akan melihat pengalaman masuk yang menawarkan pengguna kemampuan untuk keluar lagi.

4ed811a25b0cf816.png

Dan dengan itu, Anda telah menerapkan alur otentikasi. Selamat!

6. Tulis pesan ke Cloud Firestore

Mengetahui bahwa pengguna akan datang adalah hal yang bagus, tetapi mari berikan tamu hal lain untuk dilakukan di aplikasi. Bagaimana jika mereka bisa meninggalkan pesan di buku tamu? Mereka dapat berbagi mengapa mereka bersemangat untuk datang atau siapa yang ingin mereka temui.

Untuk menyimpan pesan chat yang pengguna menulis dalam aplikasi, Anda akan menggunakan Cloud Firestore .

Model data

Cloud Firestore adalah database NoSQL, dan data yang disimpan dalam database dibagi menjadi koleksi, dokumen, kolom, dan subkoleksi. Anda akan menyimpan setiap pesan chat sebagai dokumen dalam koleksi tingkat atas disebut guestbook .

7c20dc8424bb1d84.png

Tambahkan pesan ke Firestore

Di bagian ini, Anda akan menambahkan fungsionalitas bagi pengguna untuk menulis pesan baru ke database. Pertama, Anda menambahkan elemen UI (bidang formulir dan tombol kirim), lalu Anda menambahkan kode yang mengaitkan elemen-elemen ini ke database.

Pertama, tambahkan impor untuk cloud_firestore paket dan dart:async .

lib/main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

Untuk membangun elemen UI dari kolom pesan dan tombol kirim, menambahkan stateful baru widget GuestBook di bagian bawah lib/main.dart .

lib/main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage});
  final FutureOr<void> Function(String message) addMessage;

  @override
  _GuestBookState createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Ada beberapa tempat menarik di sini. Pertama, Anda membuat Instansi Formulir sehingga upi dapat memvalidasi pesan yang sebenarnya memiliki beberapa konten, dan menunjukkan kepada pengguna pesan kesalahan jika tidak ada. Cara untuk memvalidasi formulir melibatkan mengakses negara berupa belakang formulir, dan untuk ini Anda menggunakan GlobalKey . Untuk informasi lebih lanjut tentang Keys, dan bagaimana menggunakannya, silakan lihat Flutter Widgets 101 episode "Kapan Gunakan Keys" .

Perhatikan juga cara widget diletakkan, Anda memiliki Row , dengan TextFormField dan StyledButton , yang itu sendiri berisi Row . Juga perhatikan TextFormField dibungkus dalam sebuah Expanded widget, pasukan ini TextFormField untuk mengambil setiap ruang ekstra di baris. Untuk lebih memahami mengapa hal ini diperlukan, silakan membaca kendala Memahami .

Sekarang setelah Anda memiliki widget yang memungkinkan pengguna memasukkan beberapa teks untuk ditambahkan ke Buku Tamu, Anda perlu menampilkannya di layar. Untuk melakukannya, mengedit tubuh HomePage untuk menambahkan dua baris berikut di bagian bawah ListView 's anak:

const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

Meskipun ini cukup untuk menampilkan Widget, itu tidak cukup untuk melakukan sesuatu yang berguna. Anda akan segera memperbarui kode ini agar berfungsi.

Pratinjau aplikasi

Seorang pengguna mengklik tombol SEND akan memicu potongan kode di bawah. Ia menambahkan isi dari field input pesan ke guestbook koleksi database. Secara khusus, addMessageToGuestBook metode menambah isi pesan ke dokumen baru (dengan ID secara otomatis) ke guestbook koleksi.

Perhatikan bahwa FirebaseAuth.instance.currentUser.uid adalah referensi ke auto-generated ID unik yang Firebase Authentication memberikan untuk semua log-in pengguna.

Membuat perubahan lain ke lib/main.dart berkas. Tambahkan addMessageToGuestBook metode. Anda akan menghubungkan antarmuka pengguna dan kemampuan ini bersama-sama di langkah berikutnya.

lib/main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (_loginState != ApplicationLoginState.loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // To here
}

Menghubungkan UI ke database

Anda memiliki UI tempat pengguna dapat memasukkan teks yang ingin mereka tambahkan ke Buku Tamu, dan Anda memiliki kode untuk menambahkan entri ke Cloud Firestore. Sekarang yang perlu Anda lakukan hanyalah menyambungkan keduanya. Dalam lib/main.dart membuat perubahan berikut ke HomePage widget.

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loginState == ApplicationLoginState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

Anda telah mengganti dua baris yang Anda tambahkan kembali di awal langkah ini dengan implementasi penuh. Anda lagi menggunakan Consumer<ApplicationState> untuk membuat negara aplikasi yang tersedia untuk bagian dari pohon Anda rendering. Ini memungkinkan Anda untuk bereaksi terhadap seseorang yang memasukkan pesan di UI, dan memublikasikannya ke dalam database. Di bagian berikutnya Anda akan menguji apakah pesan yang ditambahkan dipublikasikan ke dalam database.

Uji pengiriman pesan

  1. Pastikan Anda masuk ke aplikasi.
  2. Masukkan pesan seperti "Hei!", Lalu klik KIRIM.

Tindakan ini menulis pesan ke database Cloud Firestore Anda. Namun, Anda belum akan melihat pesan tersebut di aplikasi Flutter Anda yang sebenarnya karena Anda masih perlu menerapkan pengambilan data. Anda akan melakukannya di langkah berikutnya.

Tetapi Anda dapat melihat pesan yang baru ditambahkan di konsol Firebase.

Dalam Firebase konsol, di database dashboard , Anda harus melihat guestbook koleksi dengan pesan Anda baru ditambahkan. Jika Anda terus mengirim pesan, koleksi buku tamu Anda akan berisi banyak dokumen, seperti ini:

Konsol Firebase

713870af0b3b63c.png

7. Baca pesan

Sangat menyenangkan bahwa para tamu dapat menulis pesan ke database, tetapi mereka belum dapat melihatnya di aplikasi. Mari kita perbaiki itu!

Sinkronkan pesan

Untuk menampilkan pesan, Anda harus menambahkan listener yang memicu saat data berubah, lalu membuat elemen UI yang menampilkan pesan baru. Anda akan menambahkan kode ke status aplikasi yang mendengarkan pesan yang baru ditambahkan dari aplikasi.

Tepat di atas GuestBook widget kelas nilai berikut. Kelas ini menampilkan tampilan terstruktur dari data yang Anda simpan di Cloud Firestore.

lib/main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}

Pada bagian dari ApplicationState mana Anda mendefinisikan negara dan getter, tambahkan baris baru berikut:

lib/main.dart

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

Dan akhirnya, di bagian inisialisasi ApplicationState , tambahkan berikut untuk berlangganan query atas koleksi dokumen ketika pengguna log in, dan berhenti berlangganan ketika mereka log out.

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        // Add from here
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

Bagian ini penting, karena di sini adalah di mana Anda membangun sebuah permintaan atas guestbook koleksi, dan menangani berlangganan dan berhenti berlangganan untuk koleksi ini. Anda mendengarkan aliran, di mana Anda merekonstruksi cache lokal dari pesan dalam guestbook koleksi, dan juga menyimpan referensi ke langganan ini sehingga Anda dapat berhenti berlangganan nanti. Ada banyak hal yang terjadi di sini, dan ada baiknya menghabiskan waktu di debugger untuk memeriksa apa yang terjadi ketika mendapatkan model mental yang lebih jelas.

Untuk informasi lebih lanjut, lihat dokumentasi Cloud Firestore .

Dalam GuestBook widget Anda perlu menghubungkan negara mengubah ini untuk user interface. Anda memodifikasi widget dengan menambahkan daftar pesan sebagai bagian dari konfigurasinya.

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({required this.addMessage, required this.messages});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

Berikutnya, kita mengekspos konfigurasi baru ini di _GuestBookState dengan memodifikasi build metode sebagai berikut.

lib/main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // to here.
    );
  }
}

Anda membungkus isi sebelumnya metode membangun dengan Column widget, dan kemudian di ekor Column 's anak Anda menambahkan koleksi untuk untuk menghasilkan baru Paragraph untuk setiap pesan dalam daftar pesan.

Akhirnya, Anda sekarang perlu untuk memperbarui tubuh HomePage untuk benar membangun GuestBook dengan baru messages parameter.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

Uji sinkronisasi pesan

Cloud Firestore secara otomatis dan instan menyinkronkan data dengan klien yang berlangganan database.

  1. Pesan yang Anda buat sebelumnya di database harus ditampilkan di aplikasi. Jangan ragu untuk menulis pesan baru; mereka akan muncul secara instan.
  2. Jika Anda membuka ruang kerja di beberapa jendela atau tab, pesan akan disinkronkan secara waktu nyata di seluruh tab.
  3. (Opsional) Anda dapat mencoba secara manual menghapus, memodifikasi, atau menambahkan pesan baru secara langsung di bagian Database konsol Firebase; perubahan apa pun akan muncul di UI.

Selamat! Anda sedang membaca dokumen Cloud Firestore di aplikasi Anda!

App p tinjauan

8. Siapkan aturan keamanan dasar

Anda awalnya menyiapkan Cloud Firestore untuk menggunakan mode pengujian, artinya database Anda terbuka untuk membaca dan menulis. Namun, Anda hanya boleh menggunakan mode uji selama tahap pengembangan yang sangat awal. Sebagai praktik terbaik, Anda harus menyiapkan aturan keamanan untuk database saat mengembangkan aplikasi. Keamanan harus menjadi bagian integral dari struktur dan perilaku aplikasi Anda.

Aturan Keamanan memungkinkan Anda mengontrol akses ke dokumen dan koleksi di database Anda. Sintaks aturan yang fleksibel memungkinkan Anda membuat aturan yang cocok dengan apa pun, mulai dari semua penulisan ke seluruh database hingga operasi pada dokumen tertentu.

Anda dapat menulis aturan keamanan untuk Cloud Firestore di Firebase console:

  1. Dalam Mengembangkan bagian Firebase konsol, klik Database, kemudian pilih tab Aturan (atau klik disini untuk langsung ke tab Aturan).
  2. Anda akan melihat aturan keamanan default berikut, bersama dengan peringatan tentang aturan yang bersifat publik.

7767a2d2e64e7275.png

Identifikasi koleksi

Pertama, identifikasi koleksi tempat aplikasi menulis data.

Dalam match /databases/{database}/documents , mengidentifikasi koleksi yang Anda ingin mengamankan:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

Tambahkan aturan keamanan

Karena Anda menggunakan UID Otentikasi sebagai bidang di setiap dokumen buku tamu, Anda bisa mendapatkan UID Otentikasi dan memverifikasi bahwa siapa pun yang mencoba menulis ke dokumen memiliki UID Otentikasi yang cocok.

Tambahkan aturan baca dan tulis ke kumpulan aturan Anda seperti yang ditunjukkan di bawah ini:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

Sekarang, untuk buku tamu, hanya pengguna yang masuk yang dapat membaca pesan (pesan apa pun!), tetapi hanya pembuat pesan yang dapat mengedit pesan.

Tambahkan aturan validasi

Tambahkan validasi data untuk memastikan bahwa semua bidang yang diharapkan ada dalam dokumen:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. Langkah bonus: Latih apa yang telah Anda pelajari

Catat status RSVP peserta

Saat ini, aplikasi Anda hanya memungkinkan orang untuk mulai mengobrol jika mereka tertarik dengan acara tersebut. Juga, satu-satunya cara Anda tahu jika seseorang datang adalah jika mereka mempostingnya di obrolan. Mari kita mengatur dan memberi tahu orang-orang berapa banyak orang yang datang.

Anda akan menambahkan beberapa kemampuan baru ke status aplikasi. Yang pertama adalah kemampuan bagi pengguna yang masuk untuk mencalonkan apakah mereka hadir atau tidak. Kemampuan kedua adalah penghitung berapa banyak orang yang benar-benar hadir.

Dalam lib/main.dart , tambahkan berikut ke bagian accesor untuk memungkinkan kode UI untuk berinteraksi dengan keadaan ini:

lib/main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}

Perbarui ApplicationState 's init metode sebagai berikut:

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

Di atas menambahkan kueri selalu berlangganan untuk mengetahui jumlah peserta, dan kueri kedua yang hanya aktif saat pengguna masuk untuk mengetahui apakah pengguna hadir. Berikutnya, tambahkan pencacahan berikut setelah GuestBookMessage deklarasi:

lib/main.dart

enum Attending { yes, no, unknown }

Anda sekarang akan menentukan widget baru yang berfungsi seperti tombol radio lama. Ini dimulai dalam keadaan tak tentu, dengan tidak memilih ya atau tidak, tetapi setelah pengguna memilih apakah mereka hadir atau tidak, maka Anda menunjukkan opsi yang disorot dengan tombol yang diisi, dan opsi lainnya surut dengan rendering datar.

lib/main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection({required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

Berikutnya, Anda perlu memperbarui HomePage 's metode membangun untuk mengambil keuntungan dari YesNoSelection , memungkinkan login pengguna untuk mencalonkan jika mereka menghadiri. Anda juga akan menampilkan jumlah peserta untuk acara ini.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // To here.
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

Tambahkan aturan

Karena Anda sudah menyiapkan beberapa aturan, data baru yang Anda tambahkan dengan tombol akan ditolak. Anda harus memperbarui aturan untuk memungkinkan menambah attendees koleksi.

Untuk attendees koleksi, karena Anda menggunakan Authentication UID sebagai nama dokumen, Anda bisa ambil dan memverifikasi bahwa submitter ini uid adalah sama dengan dokumen yang mereka tulis. Anda akan mengizinkan semua orang membaca daftar peserta (karena tidak ada data pribadi di sana), tetapi hanya pembuatnya yang dapat memperbaruinya.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Tambahkan aturan validasi

Tambahkan validasi data untuk memastikan bahwa semua bidang yang diharapkan ada dalam dokumen:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}

(Opsional) Anda sekarang dapat melihat hasil mengklik tombol. Buka dasbor Cloud Firestore Anda di Firebase console.

Pratinjau aplikasi

10. Selamat!

Anda telah menggunakan Firebase untuk membuat aplikasi web waktu nyata yang interaktif!

Apa yang telah kita bahas

  • Otentikasi Firebase
  • Cloud Firestore
  • Aturan Keamanan Firebase

Langkah selanjutnya

  • Ingin mempelajari lebih lanjut tentang produk Firebase lainnya? Mungkin Anda ingin menyimpan file gambar yang diunggah pengguna? Atau mengirim pemberitahuan ke pengguna Anda? Memeriksa dokumentasi Firebase . Ingin mempelajari lebih lanjut tentang plugin Flutter untuk Firebase? Periksa FlutterFire untuk informasi lebih lanjut.
  • Ingin mempelajari lebih lanjut tentang Cloud Firestore? Mungkin Anda ingin belajar tentang subkoleksi dan transaksi? Kepala ke codelab web Cloud Firestore untuk codelab yang masuk ke lebih mendalam tentang Cloud Firestore. Atau periksa ini seri YouTube untuk mengenal Cloud Firestore !

Belajarlah lagi

Bagaimana hasilnya?

Kami akan menyukai tanggapan Anda! Silahkan isi (sangat) bentuk pendek di sini .