Cara Membangun Aplikasi Seluler Full-Stack Dengan Flutter, Fauna, dan GraphQL

Flutter adalah framework UI Google yang digunakan untuk membuat aplikasi seluler lintas platform yang ekspresif dan fleksibel. Ini adalah salah satu kerangka kerja yang tumbuh paling cepat untuk pengembangan aplikasi seluler. Di sisi lain, Fauna adalah database tanpa server yang transaksional dan ramah pengembang yang mendukung GraphQL asli. Flutter + Fauna adalah pertandingan yang dibuat di Surga. Jika Anda ingin membuat dan mengirimkan aplikasi full-stack yang kaya fitur dalam waktu singkat, Flutter dan Fauna adalah alat yang tepat untuk pekerjaan itu. Dalam artikel ini, kami akan memandu Anda dalam membangun aplikasi Flutter pertama Anda dengan Fauna dan GraphQL back-end.

Anda dapat menemukan kode lengkap untuk artikel ini, di GitHub.

Tujuan pembelajaran

Pada akhir artikel ini, Anda harus mengetahui cara:

  1. atur instance Fauna,
  2. buat skema GraphQL untuk Fauna,
  3. atur klien GraphQL di aplikasi Flutter, dan
  4. melakukan query dan mutasi terhadap Fauna GraphQL back-end.

Fauna vs. Amplifikasi AWS vs. Firebase: Masalah apa yang diselesaikan Fauna? Apa bedanya dengan solusi tanpa server lainnya? Jika Anda baru mengenal Fauna dan ingin mempelajari lebih lanjut tentang bagaimana Fauna dibandingkan dengan solusi lain, saya sarankan membaca artikel ini.

Apa yang kita bangun?

Kami akan membangun aplikasi seluler sederhana yang memungkinkan pengguna untuk menambah, menghapus, dan memperbarui karakter favorit mereka dari film dan acara.

Menyiapkan Fauna

Buka fauna.com dan buat akun baru. Setelah login, Anda harus dapat membuat database baru.

Beri nama untuk database Anda. Saya akan menamai saya flutter_demo. Selanjutnya, kita dapat memilih grup wilayah. Untuk demo ini, kami akan memilih klasik. Fauna adalah database tanpa server yang didistribusikan secara global. Ini adalah satu-satunya database yang mendukung akses baca dan tulis dengan latensi rendah dari mana saja. Anggap saja sebagai CDN (Content Delivery Network) tetapi untuk database Anda. Untuk mempelajari lebih lanjut tentang grup wilayah, ikuti panduan ini.

Membuat kunci admin

Setelah database dibuat, buka tab keamanan. Klik tombol kunci baru dan buat kunci baru untuk database Anda. Simpan kunci ini dengan aman karena kami membutuhkan ini untuk operasi GraphQL kami.

Kami akan membuat kunci admin untuk database kami. Kunci dengan peran admin digunakan untuk mengelola database terkait, termasuk penyedia akses database, database anak, dokumen, fungsi, indeks, kunci, token, dan peran yang ditentukan pengguna. Anda dapat mempelajari lebih lanjut tentang berbagai kunci keamanan dan peran akses Fauna di tautan berikut.

Buat skema GraphQL

Kami akan membangun aplikasi sederhana yang memungkinkan pengguna untuk menambah, memperbarui, dan menghapus karakter TV favorit mereka.

Membuat proyek Flutter baru

Mari buat proyek flutter baru dengan menjalankan perintah berikut.

flutter create my_app

Di dalam direktori proyek, kita akan membuat file baru bernama graphql/schema.graphql.

Dalam file skema, kami akan menentukan struktur koleksi kami. Koleksi di Fauna mirip dengan tabel di SQL. Kami hanya membutuhkan satu koleksi untuk saat ini. Kami akan menyebutnya Character.

### schema.graphql
type Character {
    name: String!
    description: String!
    picture: String
}
type Query {
    listAllCharacters: [Character]
}

Seperti yang Anda lihat di atas, kami mendefinisikan tipe yang disebut Character dengan beberapa sifat (yaitu, name, description, picture, dll.). Pikirkan properti sebagai kolom database SQL atau nilai kunci yang dibayarkan dari database NoSQL. Kami juga telah mendefinisikan Query. Kueri ini akan mengembalikan daftar karakter.

Sekarang mari kita kembali ke dashboard Fauna. Klik GraphQL dan klik skema impor untuk mengunggah skema kami ke Fauna.

Setelah pengimporan selesai, kita akan melihat bahwa Fauna telah menghasilkan kueri dan mutasi GraphQL.

Tidak suka GraphQL yang dibuat secara otomatis? Ingin lebih mengontrol logika bisnis Anda? Dalam hal ini, Fauna memungkinkan Anda untuk menentukan resolver GraphQL kustom Anda. Untuk mempelajari lebih lanjut, ikuti tautan ini.

Siapkan klien GraphQL di aplikasi Flutter

Mari kita buka pubspec.yaml file dan tambahkan dependensi yang diperlukan.

...
dependencies:
  graphql_flutter: ^4.0.0-beta
  hive: ^1.3.0
  flutter:
    sdk: flutter
...

Kami menambahkan dua dependensi di sini. graphql_flutter adalah pustaka klien GraphQL untuk flutter. Ini membawa semua fitur modern klien GraphQL ke dalam satu paket yang mudah digunakan. Kami juga menambahkan hive paket sebagai ketergantungan kita. Hive adalah basis data nilai kunci ringan yang ditulis dalam Dart murni untuk penyimpanan lokal. Kami menggunakan Hive untuk men-cache kueri GraphQL kami.

Selanjutnya, kita akan membuat file baru lib/client_provider.dart. Kami akan membuat kelas penyedia di file ini yang akan berisi konfigurasi Fauna kami.

Untuk terhubung ke GraphQL API Fauna, pertama-tama kita perlu membuat GraphQLClient. GraphQLClient memerlukan cache dan tautan untuk diinisialisasi. Mari kita lihat kode di bawah ini.

// lib/client_provider.dart
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:flutter/material.dart';

ValueNotifier<GraphQLClient> clientFor({
  @required String uri,
  String subscriptionUri,
}) {

  final HttpLink httpLink = HttpLink(
    uri,
  );
  final AuthLink authLink = AuthLink(
    getToken: () async => 'Bearer fnAEPAjy8QACRJssawcwuywad2DbB6ssrsgZ2-2',
  );
  Link link = authLink.concat(httpLink);
  return ValueNotifier<GraphQLClient>(
    GraphQLClient(
      cache: GraphQLCache(store: HiveStore()),
      link: link,
    ),
  );
} 

Dalam kode di atas, kami membuat ValueNotifier untuk membungkus GraphQLClient. Perhatikan bahwa kami mengonfigurasi AuthLink di baris 13 – 15 (disorot). Pada baris 14, kami telah menambahkan kunci admin dari Fauna sebagai bagian dari token. Di sini saya telah membuat hardcode kunci admin. Namun, dalam aplikasi produksi, kita harus menghindari hard-coding kunci keamanan apa pun dari Fauna.

Ada beberapa cara untuk menyimpan rahasia di aplikasi Flutter. Silakan lihat posting blog ini untuk referensi.

Kami ingin dapat menelepon Query dan Mutation dari widget aplikasi kita. Untuk melakukannya kita perlu membungkus widget kita dengan GraphQLProvider widget.

// lib/client_provider.dart

....

/// Wraps the root application with the `graphql_flutter` client.
/// We use the cache for all state management.
class ClientProvider extends StatelessWidget {
  ClientProvider({
    @required this.child,
    @required String uri,
  }) : client = clientFor(
          uri: uri,
        );
  final Widget child;
  final ValueNotifier<GraphQLClient> client;
  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
      client: client,
      child: child,
    );
  }
}

Selanjutnya, kita pergi ke main.dart file dan bungkus widget utama kami dengan ClientProvider widget. Mari kita lihat kode di bawah ini.

// lib/main.dart
...

void main() async {
  await initHiveForFlutter();
  runApp(MyApp());
}
final graphqlEndpoint="https://graphql.fauna.com/graphql";
class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ClientProvider(
      uri: graphqlEndpoint,
      child: MaterialApp(
        title: 'My Character App',
        debugShowCheckedModeBanner: false,
        initialRoute: "https://css-tricks.com/",
        routes: {
          "https://css-tricks.com/": (_) => AllCharacters(),
          '/new': (_) => NewCharacter(),
        }
      ),
    );
  }
}

Pada titik ini, semua widget hilir kami akan memiliki akses untuk dijalankan Queries dan Mutations berfungsi dan dapat berinteraksi dengan GraphQL API.

halaman aplikasi

Aplikasi demo harus sederhana dan mudah diikuti. Mari kita lanjutkan dan buat widget daftar sederhana yang akan menampilkan daftar semua karakter. Ayo buat file baru lib/screens/character-list.dart. Dalam file ini, kita akan menulis widget baru bernama AllCharacters.

// lib/screens/character-list.dart.dart

class AllCharacters extends StatelessWidget {
  const AllCharacters({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            snap: false,
            floating: true,
            expandedHeight: 160.0,
            title: Text(
              'Characters',
              style: TextStyle(
                fontWeight: FontWeight.w400, 
                fontSize: 36,
              ),
            ),
            actions: <Widget>[
              IconButton(
                padding: EdgeInsets.all(5),
                icon: const Icon(Icons.add_circle),
                tooltip: 'Add new entry',
                onPressed: () { 
                  Navigator.pushNamed(context, '/new');
                },
              ),
            ],
          ),
          SliverList(
            delegate: SliverChildListDelegate([
                Column(
                  children: [
                    for (var i = 0; i < 10; i++) 
                      CharacterTile()
                  ],
                )
            ])
          )
        ],
      ),
    );
  }
}

// Character-tile.dart
class CharacterTile extends StatefulWidget {
  CharacterTilee({Key key}) : super(key: key);
  @override
  _CharacterTileState createState() => _CharacterTileeState();
}
class _CharacterTileState extends State<CharacterTile> {
  @override
  Widget build(BuildContext context) {
    return Container(
       child: Text(&quot;Character Tile&quot;),
    );
  }
}

Seperti yang Anda lihat pada kode di atas, [line 37] kami memiliki for loop untuk mengisi daftar dengan beberapa data palsu. Akhirnya, kami akan membuat kueri GraphQL ke backend Fauna kami dan mengambil semua karakter dari database. Sebelum kita melakukannya, mari kita coba menjalankan aplikasi kita apa adanya. Kita dapat menjalankan aplikasi kita dengan perintah berikut

flutter run

Pada titik ini kita harus dapat melihat layar berikut.

Melakukan query dan mutasi

Sekarang kita memiliki beberapa widget dasar, kita dapat melanjutkan dan menghubungkan kueri GraphQL. Alih-alih string yang di-hardcode, kami ingin mendapatkan semua karakter dari database kami dan melihatnya di AllCharacters widget.

Mari kembali ke taman bermain GraphQL Fauna. Perhatikan bahwa kita dapat menjalankan kueri berikut untuk mendaftar semua karakter.

query ListAllCharacters {
  listAllCharacters(_size: 100) {
    data {
      _id
      name
      description
      picture
    }
    after
  }
}

Untuk melakukan kueri ini dari widget kami, kami perlu membuat beberapa perubahan padanya.

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:todo_app/screens/Character-tile.dart';

String readCharacters = ";";";
query ListAllCharacters {
  listAllCharacters(_size: 100) {
    data {
      _id
      name
      description
      picture
    }
    after
  }
}
";";";;

class AllCharacters extends StatelessWidget {
  const AllCharacters({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            snap: false,
            floating: true,
            expandedHeight: 160.0,
            title: Text(
              'Characters',
              style: TextStyle(
                fontWeight: FontWeight.w400, 
                fontSize: 36,
              ),
            ),
            actions: <Widget>[
              IconButton(
                padding: EdgeInsets.all(5),
                icon: const Icon(Icons.add_circle),
                tooltip: 'Add new entry',
                onPressed: () { 
                  Navigator.pushNamed(context, '/new');
                },
              ),
            ],
          ),
          SliverList(
            delegate: SliverChildListDelegate([
              Query(options: QueryOptions(
                document: gql(readCharacters), // graphql query we want to perform
                pollInterval: Duration(seconds: 120), // refetch interval
              ), 
              builder: (QueryResult result, { VoidCallback refetch, FetchMore fetchMore }) {
                if (result.isLoading) {
                  return Text('Loading');
                }
                return Column(
                  children: [
                    for (var item in result.data['listAllCharacters']['data'])
                      CharacterTile(Character: item, refetch: refetch),
                  ],
                );
              })
            ])
          )
        ],
      ),
    );
  }
} 

Pertama-tama, kami mendefinisikan string kueri untuk mendapatkan semua karakter dari database [line 5 to 17]. Kami telah membungkus widget daftar kami dengan widget Kueri dari flutter_graphql.

Jangan ragu untuk melihat dokumentasi resmi untuk perpustakaan flutter_graphql.

Dalam argumen opsi kueri, kami menyediakan string kueri GraphQL itu sendiri. Kita dapat memasukkan nomor float apa pun untuk argumen pollInterval. Interval Poll menentukan seberapa sering kita ingin mengambil kembali data dari backend kita. Widget ini juga memiliki fungsi pembangun standar. Kita dapat menggunakan fungsi pembangun untuk meneruskan hasil kueri, mengambil kembali fungsi panggilan balik, dan mengambil lebih banyak fungsi panggilan balik di pohon widget.

Selanjutnya, saya akan memperbarui CharacterTile widget untuk menampilkan data karakter di layar.

// lib/screens/character-tile.dart
...
class CharacterTile extends StatelessWidget {
  final Character;
  final VoidCallback refetch;
  final VoidCallback updateParent;
  const CharacterTile({
    Key key, 
    @required this.Character, 
    @required this.refetch,
    this.updateParent,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Row(
          children: [
            Container(
              height: 90,
              width: 90,
              decoration: BoxDecoration(
                color: Colors.amber,
                borderRadius: BorderRadius.circular(15),
                image: DecorationImage(
                  fit: BoxFit.cover,
                  image: NetworkImage(Character['picture'])
                )
              ),
            ),
            SizedBox(width: 10),
            Expanded(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    Character['name'],
                    style: TextStyle(
                      color: Colors.black87,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  SizedBox(height: 5),
                  Text(
                    Character['description'],
                    style: TextStyle(
                      color: Colors.black87,
                    ),
                    maxLines: 2,
                  ),
                ],
              )
            )
          ],
        ),
      ),
    );
  }
}

Menambahkan data baru

Kita dapat menambahkan karakter baru ke database kita dengan menjalankan mutasi di bawah ini.

mutation CreateNewCharacter($data: CharacterInput!) {
    createCharacter(data: $data) {
      _id
      name
      description
      picture
    }
}

Untuk menjalankan mutasi ini dari widget kami, kami dapat menggunakan Mutation widget dari flutter_graphql Perpustakaan. Mari buat widget baru dengan formulir sederhana untuk berinteraksi dengan pengguna dan memasukkan data. Setelah formulir diserahkan, createCharacter mutasi akan dipanggil.

// lib/screens/new.dart
...
String addCharacter = ";";";
  mutation CreateNewCharacter($data: CharacterInput!) {
    createCharacter(data: $data) {
      _id
      name
      description
      picture
    }
  }
";";";;
class NewCharacter extends StatelessWidget {
  const NewCharacter({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Add New Character'),
      ),
      body: AddCharacterForm()
    );
  }
}
class AddCharacterForm extends StatefulWidget {
  AddCharacterForm({Key key}) : super(key: key);
  @override
  _AddCharacterFormState createState() => _AddCharacterFormState();
}
class _AddCharacterFormState extends State<AddCharacterForm> {
  String name;
  String description;
  String imgUrl;
  @override
  Widget build(BuildContext context) {
    return Form(
      child: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextField(
              decoration: const InputDecoration(
                icon: Icon(Icons.person),
                labelText: 'Name *',
              ),
              onChanged: (text) {
                name = text;
              },
            ),
            TextField(
              decoration: const InputDecoration(
                icon: Icon(Icons.post_add),
                labelText: 'Description',
              ),
              minLines: 4,
              maxLines: 4,
              onChanged: (text) {
                description = text;
              },
            ),
            TextField(
              decoration: const InputDecoration(
                icon: Icon(Icons.image),
                labelText: 'Image Url',
              ),
              onChanged: (text) {
                imgUrl = text;
              },
            ),
            SizedBox(height: 20),
            Mutation(
              options: MutationOptions(
                document: gql(addCharacter),
                onCompleted: (dynamic resultData) {
                  print(resultData);
                  name="";
                  description = '';
                  imgUrl="";
                  Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => AllCharacters())
                  );
                },
              ), 
              builder: (
                RunMutation runMutation,
                QueryResult result,
              ) {
                return Center(
                  child: ElevatedButton(
                    child: const Text('Submit'),
                    onPressed: () {
                      runMutation({
                        'data': {
                          ";picture";: imgUrl,
                          ";name";: name,
                          ";description";: description,
                        }
                      });
                    },
                  ),
                );
              }
            )
          ],
        ),
      ),
    );
  }
}

Seperti yang Anda lihat dari kode di atas, widget Mutation bekerja sangat mirip dengan widget Query. Selain itu, widget Mutation memberi kita fungsi onComplete. Fungsi ini mengembalikan hasil yang diperbarui dari database setelah mutasi selesai.

Menghapus data

Untuk menghapus karakter dari database kami, kami dapat menjalankan deleteCharacter mutasi. Kami dapat menambahkan fungsi mutasi ini ke kami CharacterTile dan nyalakan saat tombol ditekan.

// lib/screens/character-tile.dart
...

String deleteCharacter = ";";";
  mutation DeleteCharacter($id: ID!) {
    deleteCharacter(id: $id) {
      _id
      name
    }
  }
";";";;

class CharacterTile extends StatelessWidget {
  final Character;
  final VoidCallback refetch;
  final VoidCallback updateParent;
  const CharacterTile({
    Key key, 
    @required this.Character, 
    @required this.refetch,
    this.updateParent,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        showModalBottomSheet(
          context: context,
          builder: (BuildContext context) {
            print(Character['picture']);
            return Mutation(
              options: MutationOptions(
                document: gql(deleteCharacter),
                onCompleted: (dynamic resultData) {
                  print(resultData);
                  this.refetch();
                },
              ), 
              builder: (
                RunMutation runMutation,
                QueryResult result,
              ) {
                return Container(
                  height: 400,
                  padding: EdgeInsets.all(30),
                  child: Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      mainAxisSize: MainAxisSize.min,
                      children: <Widget>[
                        Text(Character['description']),
                        ElevatedButton(
                          child: Text('Delete Character'),
                          onPressed: () {
                            runMutation({
                              'id': Character['_id'],
                            });
                            Navigator.pop(context);
                          },
                        ),
                      ],
                    ),
                  ),
                ); 
              }
            );
          }
        );
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Row(
          children: [
            Container(
              height: 90,
              width: 90,
              decoration: BoxDecoration(
                color: Colors.amber,
                borderRadius: BorderRadius.circular(15),
                image: DecorationImage(
                  fit: BoxFit.cover,
                  image: NetworkImage(Character['picture'])
                )
              ),
            ),
            SizedBox(width: 10),
            Expanded(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    Character['name'],
                    style: TextStyle(
                      color: Colors.black87,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  SizedBox(height: 5),
                  Text(
                    Character['description'],
                    style: TextStyle(
                      color: Colors.black87,
                    ),
                    maxLines: 2,
                  ),
                ],
              )
            )
          ],
        ),
      ),
    );
  }
}

Mengedit data

Mengedit data berfungsi sama seperti menambah dan menghapus. Ini hanyalah mutasi lain di GraphQL API. Kita dapat membuat widget edit bentuk karakter yang mirip dengan widget bentuk karakter baru. Satu-satunya perbedaan adalah bahwa formulir edit akan berjalan updateCharacter mutasi. Untuk mengedit saya membuat widget baru lib/screens/edit.dart. Berikut kode untuk widget ini.

// lib/screens/edit.dart

String editCharacter = """
mutation EditCharacter($name: String!, $id: ID!, $description: String!, $picture: String!) {
  updateCharacter(data: 
  { 
    name: $name 
    description: $description
    picture: $picture
  }, id: $id) {
    _id
    name
    description
    picture
  }
}
""";
class EditCharacter extends StatelessWidget {
  final Character;
  const EditCharacter({Key key, this.Character}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Edit Character'),
      ),
      body: EditFormBody(Character: this.Character),
    );
  }
}
class EditFormBody extends StatefulWidget {
  final Character;
  EditFormBody({Key key, this.Character}) : super(key: key);
  @override
  _EditFormBodyState createState() => _EditFormBodyState();
}
class _EditFormBodyState extends State<EditFormBody> {
  String name;
  String description;
  String picture;
  @override
  Widget build(BuildContext context) {
    return Container(
       child: Padding(
         padding: const EdgeInsets.all(8.0),
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
            TextFormField(
               initialValue: widget.Character['name'],
                decoration: const InputDecoration(
                  icon: Icon(Icons.person),
                  labelText: 'Name *',
                ),
                onChanged: (text) {
                  name = text;
                }
            ),
            TextFormField(
              initialValue: widget.Character['description'],
              decoration: const InputDecoration(
                icon: Icon(Icons.person),
                labelText: 'Description',
              ),
              minLines: 4,
              maxLines: 4,
              onChanged: (text) {
                description = text;
              }
            ),
            TextFormField(
              initialValue: widget.Character['picture'],
              decoration: const InputDecoration(
                icon: Icon(Icons.image),
                labelText: 'Image Url',
              ),
              onChanged: (text) {
                picture = text;
              },
            ),
            SizedBox(height: 20),
            Mutation(
              options: MutationOptions(
                document: gql(editCharacter),
                onCompleted: (dynamic resultData) {
                  print(resultData);
                  Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => AllCharacters())
                  );
                },
              ),
              builder: (
                RunMutation runMutation,
                QueryResult result,
              ) {
                print(result);
                return Center(
                  child: ElevatedButton(
                    child: const Text('Submit'),
                    onPressed: () {

                      runMutation({
                        'id': widget.Character['_id'],
                        'name': name != null ? name : widget.Character['name'],
                        'description': description != null ? description : widget.Character['description'],
                        'picture': picture != null ? picture : widget.Character['picture'],
                      });
                    },
                  ),
                );
              }
            ),
           ]
         )
       ),
    );
  }
}

Anda dapat melihat kode lengkap untuk artikel ini di bawah ini.

Ke mana harus pergi dari sini?

Tujuan utama dari artikel ini adalah untuk membuat Anda aktif dan berjalan dengan Flutter dan Fauna. Kami hanya menggores permukaan di sini. Ekosistem fauna menyediakan backend lengkap, penskalaan otomatis, ramah pengembang sebagai layanan untuk aplikasi seluler Anda. Jika tujuan Anda adalah mengirimkan aplikasi seluler lintas platform yang siap produksi dalam waktu singkat, berikan Fauna dan Flutter adalah cara untuk pergi.

Saya sangat merekomendasikan untuk memeriksa situs dokumentasi resmi Fauna. Jika Anda tertarik untuk mempelajari lebih lanjut tentang klien GraphQL untuk Dart/Flutter, periksa repo GitHub resmi untuk graphql_flutter.

Selamat meretas dan sampai jumpa di lain waktu.