Flutter 教程:如何創建您的第一個 Flutter 應用程序

已發表: 2022-03-11

什麼是顫振?

Flutter 是 Google 的移動應用開發 SDK,它允許您的產品同時針對 Android 和 iOS 平台,而無需維護兩個單獨的代碼庫。 此外,還可以編譯使用 Flutter 的應用程序以針對 Google 即將推出的 Fuchsia 操作系統。

Flutter 最近達到了一個重要的里程碑——穩定版 1.0。 該版本於 2018 年 12 月 5 日在倫敦的 Flutter Live 活動中發布。 雖然它仍然可以被視為一個早期且不斷發展的軟件企業,但本文將重點關註一個已經得到驗證的概念,並展示如何使用 Flutter 1.2 和 Firebase 開發一個針對主要移動平台的功能齊全的消息傳遞應用程序。

從下圖可以看出,Flutter 近幾個月來獲得了大量用戶。 2018 年,Flutter 的市場份額翻了一番,在搜索查詢方面有望超越 React Native,因此我們決定創建一個新的 Flutter 教程。

圖表比較了 2018 年 7 月至 2018 年 9 月的 Flutter 和 React 用戶。

注意:本文只關注實現的某些部分。 可以在此 GitHub 存儲庫中找到該項目的完整源代碼參考。

先決條件

儘管已經努力讓讀者能夠關注並完成這個項目,即使這是他們第一次嘗試移動開發,但在沒有詳細解釋的情況下,提到並使用了許多非 Flutter 特定的核心移動開發概念。

這是為了文章簡潔而進行的,因為其目標之一是讓讀者一次性完成項目。 最後,本文假設您已經設置好開發環境,包括所需的 Android Studio 插件和 Flutter SDK。

Firebase 設置

設置 Firebase 是我們必須為每個平台獨立完成的唯一事情。 首先,確保您在 Firebase Dashboard 中創建了一個新項目,並在新生成的工作區中添加 Android 和 iOS 應用程序。 該平台將生成您需要下載的兩個配置文件:用於 Android 的google-services.json和用於 iOS GoogleService-Info.plist 。 在關閉儀表板之前,請確保啟用 Firebase 和 Google 身份驗證提供程序,因為我們將使用它們來識別用戶。 為此,請從菜單中選擇身份驗證項目,然後選擇登錄方法選項卡。

現在您可以關閉儀表板,因為其餘設置都在我們的代碼庫中進行。 首先,我們需要把我們下載的文件放到我們的項目中。 google-services.json文件應該放在$(FLUTTER_PROJECT_ROOT)/android/app文件夾中, GoogleService-Info.plist應該放在$(FLUTTER_PROJECT_ROOT)/ios/Runner目錄中。 接下來,我們需要實際設置我們將在項目中使用的 Firebase 庫,並將它們與配置文件掛鉤。 這是通過指定我們將在項目的pubspec.yaml文件中使用的 Dart 包(庫)來完成的。 在文件的依賴項部分,粘貼以下代碼段:

 flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:

前兩個與 Firebase 無關,但將在項目中經常使用。 最後兩個是,希望,不言自明。

最後,我們需要配置特定於平台的項目設置,以使我們的身份驗證流程能夠成功完成。 在 Android 端,我們需要將 google-services Gradle 插件添加到我們的項目級 Gradle 配置中。 換句話說,我們需要在$(FLUTTER_PROJECT_ROOT)/android/build.gradle文件的依賴列表中添加以下項:

 classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version

然後我們需要通過將此行添加到$(FLUTTER_PROJECT_ROOT)/android/app/build.gradle的末尾來應用該插件:

 apply plugin: 'com.google.gms.google-services'

該平台的最後一件事是獲取您的 Facebook 應用程序參數。 我們在這裡尋找的是編輯這兩個文件 - $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml$(FLUTTER_PROJECT_ROOT)/android/app/src/main/res/values/strings.xml

 <!-- AndroidManifest.xml --> <manifest xmlns:androcom.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/> <activity android:name="com.facebook.FacebookActivity" android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" android:label="@string/app_name" /> <activity android:name="com.facebook.CustomTabActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="@string/fb_login_protocol_scheme" /> </intent-filter> </activity> <!-- … --> </application> </manifest> <!-- strings.xml --> <resources> <string name="app_name">Toptal Chat</string> <string name="facebook_app_id">${YOUR_FACEBOOK_APP_ID}</string> <string name="fb_login_protocol_scheme">${YOUR_FACEBOOK_URL}</string> </resources>

現在是 iOS 的時候了。 幸運的是,在這種情況下我們只需要更改一個文件。 將以下值(注意CFBundleURLTypes項可能已經存在於列表中;在這種情況下,您需要將這些項添加到現有數組中而不是再次聲明它)到$(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist文件:

 <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_FACEBOOK_URL}</string> </array> </dict> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_REVERSED_GOOGLE_WEB_CLIENT_ID}</string> </array> </dict> </array> <key>FacebookAppID</key> <string>${YOUR_FACEBOOK_APP_ID}</string> <key>FacebookDisplayName</key> <string>${YOUR_FACEBOOK_APP_NAME}</string> <key>LSApplicationQueriesSchemes</key> <array> <string>fbapi</string> <string>fb-messenger-share-api</string> <string>fbauth2</string> <string>fbshareextension</string> </array>

關於 BLoC 架構的一句話

這個架構標准在我們之前的一篇文章中有所描述,展示了在 Flutter 和 AngularDart 中使用 BLoC 進行代碼共享,所以我們不會在這裡詳細解釋。

主要思想背後的基本思想是每個屏幕都有以下類: -視圖- 負責顯示當前狀態並將用戶輸入作為事件委託給 bloc。 -狀態- 表示用戶使用當前視圖與之交互的“實時”數據。 - bloc - 響應事件並相應地更新狀態,可選擇從一個或多個本地或遠程存儲庫請求數據。 -事件- 這是一個確定的動作結果,可能會或可能不會改變當前狀態。

作為一個圖形表示,它可以被認為是這樣的:

Flutter 教程:BLoC 架構的圖形表示。

此外,我們有一個模型目錄,其中包含生成這些類實例的數據類和存儲庫。

用戶界面開發

使用 Flutter 創建 UI 完全在 Dart 中完成,而不是在 Android 和 iOS 中使用 XML 方案構建 UI 並與業務邏輯代碼庫完全分離的原生應用程序開發。 我們將根據當前狀態(例如 isLoading、isEmpty 參數)使用具有不同組件的相對簡單的 UI 元素組合。 Flutter 中的 UI 圍繞小部件,或者更確切地說是小部件樹。 小部件可以是無狀態的或有狀態的。 當談到有狀態的時候,重要的是要強調,當setState()在當前顯示的特定小部件上調用時(在構造函數中調用它或在它被釋放後會導致運行時錯誤),構建和繪製通道是計劃在下一個繪圖週期執行。

為簡潔起見,我們將在此處僅顯示其中一個 UI(視圖)類:

 class LoginScreen extends StatefulWidget { LoginScreen({Key key}) : super(key: key); @override State<StatefulWidget> createState() => _LoginState(); } class _LoginState extends State<LoginScreen> { final _bloc = LoginBloc(); @override Widget build(BuildContext context) { return BlocProvider<LoginBloc>( bloc: _bloc, child: LoginWidget(widget: widget, widgetState: this) ); } @override void dispose() { _bloc.dispose(); super.dispose(); } } class LoginWidget extends StatelessWidget { const LoginWidget({Key key, @required this.widget, @required this.widgetState}) : super(key: key); final LoginScreen widget; final _LoginState widgetState; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Login"), ), body: BlocBuilder( bloc: BlocProvider.of<LoginBloc>(context), builder: (context, LoginState state) { if (state.loading) { return Center( child: CircularProgressIndicator(strokeWidth: 4.0) ); } else { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginGoogle(this), child: Text( "Login with Google", style: TextStyle(color: Colors.white), ), color: Colors.redAccent, ), ), ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginFacebook(this), child: Text( "Login with Facebook", style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ), ), ], ), ); } }), ); } void navigateToMain() { NavigationHelper.navigateToMain(widgetState.context); } }

其餘的 UI 類遵循相同的模式,但可能具有不同的操作,並且除了加載狀態外,還可能具有一個空的狀態小部件樹。

驗證

正如您可能已經猜到的那樣,我們將使用google_sign_influtter_facebook_login庫根據用戶的社交網絡配置文件對用戶進行身份驗證。 首先,確保將這些包導入到將處理登錄請求邏輯的文件中:

 import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';

現在,我們將有兩個獨立的部分來處理我們的身份驗證流程。 第一個將啟動 Facebook 或 Google 登錄請求:

 void onLoginGoogle(LoginWidget view) async { dispatch(LoginEventInProgress()); final googleSignInRepo = GoogleSignIn(signInOption: SignInOption.standard, scopes: ["profile", "email"]); final account = await googleSignInRepo.signIn(); if (account != null) { LoginRepo.getInstance().signInWithGoogle(account); } else { dispatch(LogoutEvent()); } } void onLoginFacebook(LoginWidget view) async { dispatch(LoginEventInProgress()); final facebookSignInRepo = FacebookLogin(); final signInResult = await facebookSignInRepo.logInWithReadPermissions(["email"]); if (signInResult.status == FacebookLoginStatus.loggedIn) { LoginRepo.getInstance().signInWithFacebook(signInResult); } else if (signInResult.status == FacebookLoginStatus.cancelledByUser) { dispatch(LogoutEvent()); } else { dispatch(LoginErrorEvent(signInResult.errorMessage)); } }

當我們從任一提供商處獲取配置文件數據時,將調用第二個。 我們將通過指示我們的登錄處理程序監聽firebase_auth onAuthStateChange流來完成此操作:

 void _setupAuthStateListener(LoginWidget view) { if (_authStateListener == null) { _authStateListener = FirebaseAuth.instance.onAuthStateChanged.listen((user) { if (user != null) { final loginProvider = user.providerId; UserRepo.getInstance().setCurrentUser(User.fromFirebaseUser(user)); if (loginProvider == "google") { // TODO analytics call for google login provider } else { // TODO analytics call for facebook login provider } view.navigateToMain(); } else { dispatch(LogoutEvent()); } }, onError: (error) { dispatch(LoginErrorEvent(error)); }); } }

UserRepoLoginRepo實現不會在此處發布,但請隨時查看 GitHub 存儲庫以獲取完整參考。

Flutter 教程:如何構建即時通訊應用

最後,我們進入有趣的部分。 顧名思義,應該盡快交換消息,理想情況下,這應該是即時的。 幸運的是, cloud_firestore允許我們與 Firestore 實例交互,我們可以使用它的snapshots()功能打開一個數據流,該數據流將為我們提供實時更新。 在我看來,除了startChatroomForUsers方法之外,所有chat_repo代碼都非常簡單。 它負責為兩個用戶創建一個新的聊天室,除非存在一個包含兩個用戶的現有聊天室(因為我們不希望擁有同一用戶對的多個實例),在這種情況下它返回現有的聊天室。

但是,由於 Firestore 的設計,它目前不支持嵌套array-contains查詢。 所以我們無法檢索到適當的數據流,但需要在我們這邊執行額外的過濾。 該解決方案包括檢索登錄用戶的所有聊天室,然後搜索也包含所選用戶的聊天室:

 Future<SelectedChatroom> startChatroomForUsers(List<User> users) async { DocumentReference userRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[1].uid); QuerySnapshot queryResults = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .where("participants", arrayContains: userRef) .getDocuments(); DocumentReference otherUserRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[0].uid); DocumentSnapshot roomSnapshot = queryResults.documents.firstWhere((room) { return room.data["participants"].contains(otherUserRef); }, orElse: () => null); if (roomSnapshot != null) { return SelectedChatroom(roomSnapshot.documentID, users[0].displayName); } else { Map<String, dynamic> chatroomMap = Map<String, dynamic>(); chatroomMap["messages"] = List<String>(0); List<DocumentReference> participants = List<DocumentReference>(2); participants[0] = otherUserRef; participants[1] = userRef; chatroomMap["participants"] = participants; DocumentReference reference = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .add(chatroomMap); DocumentSnapshot chatroomSnapshot = await reference.get(); return SelectedChatroom(chatroomSnapshot.documentID, users[0].displayName); } }

此外,由於類似的設計限制,Firebase 目前不支持具有特殊FieldValue.serverTimestamp()值的數組更新(在現有數組字段值中插入新元素)。

此值向平台表明,包含此值而不是實際值的字段應填充事務發生時服務器上的實際時間戳。 相反,我們在創建新的消息序列化對象並將該對象插入聊天室消息集合時使用DateTime.now()

 Future<bool> sendMessageToChatroom(String chatroomId, User user, String message) async { try { DocumentReference authorRef = _firestore.collection(FirestorePaths.USERS_COLLECTION).document(user.uid); DocumentReference chatroomRef = _firestore.collection(FirestorePaths.CHATROOMS_COLLECTION).document(chatroomId); Map<String, dynamic> serializedMessage = { "author" : authorRef, "timestamp" : DateTime.now(), "value" : message }; chatroomRef.updateData({ "messages" : FieldValue.arrayUnion([serializedMessage]) }); return true; } catch (e) { print(e.toString()); return false; } }

包起來

顯然,我們開發的 Flutter 消息傳遞應用程序更多的是概念驗證,而不是市場就緒的即時消息傳遞應用程序。 作為進一步發展的想法,人們可能會考慮引入端到端加密或豐富的內容(群聊、媒體附件、URL 解析)。 但在此之前,應該實現推送通知,因為它們幾乎是即時消息應用程序的必備功能,為了簡潔起見,我們已將其移出本文的範圍。 此外,Firestore 仍然缺少一些功能,以便擁有更簡單、更準確的數據,例如嵌套array-contains查詢。

正如文章開頭所提到的,Flutter 直到最近才成熟到穩定的 1.0 版本,並且將繼續增長,不僅在框架特性和功能方面,而且在開發社區和第三方庫和資源。 現在花時間熟悉 Flutter 應用程序開發是有意義的,因為它顯然可以留在這裡並加快您的移動開發過程。

無需額外費用,Flutter 開發人員也將準備好瞄準 Google 的新興操作系統——Fuchsia。

相關: Dart 語言:當 Java 和 C# 不夠鋒利時