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 教程。
注意:本文只关注实现的某些部分。 可以在此 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 创建 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_in
和flutter_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)); }); } }
UserRepo
和LoginRepo
实现不会在此处发布,但请随时查看 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。