Flutter是Google的UI框架,用於創建靈活、富有表現力的跨平台移動應用程序。它是增長最快的移動應用程序開發框架之一。另一方面,Fauna是一個事務性的、對開發者友好的無服務器數據庫,支持原生GraphQL。 Flutter Fauna是天作之合。如果您希望在創紀錄的時間內構建和發布功能豐富的全棧應用程序,Flutter和Fauna是正確的工具。在本文中,我們將引導您使用Fauna和GraphQL後端構建您的第一個Flutter應用程序。
您可以在GitHub上找到本文的完整代碼。
在閱讀完本文後,您應該知道如何:
Fauna與AWS Amplify與Firebase : Fauna解決了哪些問題?它與其他無服務器解決方案有何不同?如果您不熟悉Fauna,並且想了解更多關於Fauna與其他解決方案的比較信息,我建議您閱讀這篇文章。
我們將構建一個簡單的移動應用程序,允許用戶添加、刪除和更新他們最喜歡的電影和電視劇角色。
前往fauna.com並創建一個新帳戶。登錄後,您應該能夠創建一個新的數據庫。
為您的數據庫命名。我將我的命名為flutter_demo。接下來,我們可以選擇一個區域組。對於此演示,我們將選擇classic。 Fauna是一個全球分佈式的無服務器數據庫。它是唯一一個支持從任何地方進行低延遲讀寫訪問的數據庫。可以把它想像成CDN(內容分發網絡),但它是針對您的數據庫的。要了解有關區域組的更多信息,請遵循本指南。
數據庫創建完成後,轉到“安全”選項卡。單擊“新建密鑰”按鈕並為您的數據庫創建一個新密鑰。請妥善保管此密鑰,因為我們需要它來進行GraphQL操作。
我們將為我們的數據庫創建一個管理員密鑰。具有管理員角色的密鑰用於管理其關聯的數據庫,包括數據庫訪問提供程序、子數據庫、文檔、函數、索引、密鑰、令牌和用戶定義的角色。您可以在以下鏈接中了解有關Fauna各種安全密鑰和訪問角色的更多信息。
我們將構建一個簡單的應用程序,允許用戶添加、更新和刪除他們最喜歡的電視角色。
讓我們通過運行以下命令創建一個新的Flutter項目。
<code>flutter create my_app</code>
在項目目錄中,我們將創建一個名為graphql/schema.graphql的新文件。
在模式文件中,我們將定義集合的結構。 Fauna中的集合類似於SQL中的表。我們現在只需要一個集合。我們將它命名為Character。
<code>### schema.graphql type Character { name: String! description: String! picture: String } type Query { listAllCharacters: [Character] }</code>
如上所示,我們定義了一個名為Character的類型,它具有多個屬性(即名稱、描述、圖片等)。可以將屬性視為SQL數據庫的列或NoSQL數據庫的鍵值對。我們還定義了一個Query。此查詢將返回角色列表。
現在讓我們回到Fauna儀表板。單擊GraphQL,然後單擊導入模式以將我們的模式上傳到Fauna。
導入完成後,我們將看到Fauna生成了GraphQL查詢和變異。
不喜歡自動生成的GraphQL?想要更好地控制您的業務邏輯?在這種情況下,Fauna允許您定義自定義GraphQL解析器。要了解更多信息,請點擊此鏈接。
讓我們打開pubspec.yaml文件並添加所需的依賴項。
<code>... dependencies: graphql_flutter: ^4.0.0-beta hive: ^1.3.0 flutter: sdk: flutter ...</code>
我們在這裡添加了兩個依賴項。 graphql_flutter是Flutter的GraphQL客戶端庫。它將GraphQL客戶端的所有現代功能集成到一個易於使用的包中。我們還添加了hive包作為我們的依賴項。 Hive是用純Dart編寫的輕量級鍵值數據庫,用於本地存儲。我們使用hive來緩存我們的GraphQL查詢。
接下來,我們將創建一個新的文件lib/client_provider.dart。我們將在該文件中創建一個提供程序類,其中將包含我們的Fauna配置。
要連接到Fauna的GraphQL API,我們首先需要創建一個GraphQLClient。 GraphQLClient需要一個緩存和一個鏈接才能初始化。讓我們看一下下面的代碼。
<code>// 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, ), ); }</graphqlclient></graphqlclient></code>
在上面的代碼中,我們創建了一個ValueNotifier來包裝GraphQLClient。請注意,我們在第13-15行(突出顯示)配置了AuthLink。在第14行,我們添加了來自Fauna的管理員密鑰作為令牌的一部分。在這裡,我硬編碼了管理員密鑰。但是,在生產應用程序中,我們必須避免硬編碼Fauna的任何安全密鑰。
有幾種方法可以在Flutter應用程序中存儲密鑰。請查看此博文以供參考。
我們希望能夠從應用程序的任何小部件調用Query和Mutation。為此,我們需要使用GraphQLProvider小部件包裝我們的Widget。
<code>// lib/client_provider.dart .... /// 使用`graphql_flutter`客户端包装根应用程序。 /// 我们使用缓存进行所有状态管理。 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, ); } }</graphqlclient></code>
接下來,我們轉到main.dart文件,並使用ClientProvider小部件包裝我們的主小部件。讓我們看一下下面的代碼。
<code>// 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: '/', routes: { '/': (_) => AllCharacters(), '/new': (_) => NewCharacter(), } ), ); } }</code>
此時,我們所有的下游小部件都將能夠運行Queries和Mutations函數,並可以與GraphQL API交互。
演示應用程序應該簡單易懂。讓我們繼續創建一個簡單的列表小部件,它將顯示所有角色的列表。讓我們創建一個新的文件lib/screens/character-list.dart。在這個文件中,我們將編寫一個名為AllCharacters的新小部件。
<code>// 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 _CharacterTileeState(); } class _CharacterTileState extends State<charactertile> { @override Widget build(BuildContext context) { return Container( child: Text("Character Tile"), ); } }</charactertile></widget></code>
如上面的代碼所示,[第37行]我們有一個for循環,用一些假數據填充列表。最終,我們將對Fauna後端進行GraphQL查詢,並從數據庫中獲取所有角色。在我們這樣做之前,讓我們嘗試運行我們的應用程序。我們可以使用以下命令運行我們的應用程序
<code>flutter run</code>
此時,我們應該能夠看到以下屏幕。
現在我們有一些基本的小部件,我們可以繼續連接GraphQL查詢。我們希望從數據庫獲取所有角色,而不是硬編碼字符串,並在AllCharacters小部件中查看它們。
讓我們回到Fauna的GraphQL playground。請注意,我們可以運行以下查詢來列出所有角色。
<code>query ListAllCharacters { listAllCharacters(_size: 100) { data { _id name description picture } after } }</code>
要從我們的Widget執行此查詢,我們需要對其進行一些更改。
<code>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查询pollInterval: Duration(seconds: 120), // 重新获取间隔), 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), ], ); }) ]) ) ], ), ); } }</widget></code>
首先,我們定義了從數據庫獲取所有角色的查詢字符串[第5到17行]。我們使用flutter_graphql中的Query小部件包裝了列表小部件。
隨意查看flutter_graphql庫的官方文檔。
在query options參數中,我們提供了GraphQL查詢字符串本身。我們可以為pollInterval參數傳遞任何浮點數。 Poll Interval定義了我們希望多長時間從後端重新獲取一次數據。該小部件還有一個標準的builder函數。我們可以使用builder函數將查詢結果、重新獲取回調函數和獲取更多回調函數傳遞到Widget樹中。
接下來,我將更新CharacterTile小部件以在屏幕上顯示角色數據。
<code>// 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, ), ], ) ) ], ), ), ); } }</code>
我們可以通過運行以下變異來向我們的數據庫添加新角色。
<code>mutation CreateNewCharacter($data: CharacterInput!) { createCharacter(data: $data) { _id name description picture } }</code>
要從我們的Widget運行此變異,我們需要使用flutter_graphql庫中的Mutation小部件。讓我們創建一個帶有簡單表單的新小部件,供用戶交互和輸入數據。提交表單後,將調用createCharacter變異。
<code>// 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, } }); }, ), ); } ) ], ), ), ); } }</addcharacterform></code>
從上面的代碼可以看出,Mutation小部件的工作方式與Query小部件非常相似。此外,Mutation小部件為我們提供了一個onComplete函數。此函數在變異完成後返回數據庫中的更新結果。
我們可以通過運行deleteCharacter變異來從數據庫中刪除角色。我們可以將此變異函數添加到我們的CharacterTile中,並在按下按鈕時觸發它。
<code>// 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, ), ], ) ) ], ), ), ); } }</widget></code>
編輯數據與添加和刪除相同。它只是GraphQL API中的另一個變異。我們可以創建一個類似於新角色表單小部件的編輯角色表單小部件。唯一的區別是編輯表單將運行updateCharacter變異。為了編輯,我創建了一個新的Widget lib/screens/edit.dart。以下是此Widget的代碼。
<code>// 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'], }); }, ), ); } ), ] ) ), ); } }</editformbody></code>
您可以查看本文的完整代碼如下。
對Fauna或Flutter有疑問?您可以在Twitter @HaqueShadid上聯繫我
GitHub ### 後續步驟
本文的主要目的是讓您開始使用Flutter和Fauna。我們在這裡只觸及了表面。 Fauna生態系統為您的移動應用程序提供了一個完整的、自動縮放的、對開發者友好的後端即服務。如果您的目標是在創紀錄的時間內發布一個可用於生產的跨平台移動應用程序,請嘗試使用Fauna和Flutter 。
我強烈建議您查看Fauna的官方文檔網站。如果您有興趣了解更多關於Dart/Flutter的GraphQL客戶端的信息,請查看graphql_flutter的官方GitHub存儲庫。
祝您編程愉快,下次再見。
以上是如何使用Flutter,Fauna和GraphQL構建全堆棧移動應用程序的詳細內容。更多資訊請關注PHP中文網其他相關文章!