个性化阅读
专注于IT技术分析

Flutter教程:如何创建你的第一个Flutter应用

本文概述

什么是Flutter?

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仪表板中创建一个新项目, 并在新生成的工作区中添加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:android="http://schemas.android.com/apk/res/android>
<!-- … -->
 
    <application>
        <!-- … -->
        <meta-data android:name="com.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">srcmini Chat</string>
   <string name="facebook_app_id">${YOUR_FACEBOOK_APP_ID}</string>
   <string name="fb_login_protocol_scheme">${YOUR_FACEBOOK_URL}</string>
</resources>

现在是时候使用iOS了。幸运的是, 在这种情况下, 我们只需要更改一个文件。将以下值添加到$(FLUTTER_PROJECT)ROOT / ios / Runner / Info.plist中(请注意, 列表中可能已经存在CFBundleURLTypes项;在这种情况下, 你需要将这些项添加到现有数组中, 而不是再次声明它)。文件:

<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事件。 -状态-表示用户使用当前视图与之交互的”实时”数据。 -块-响应事件并相应地更新状态, 可以选择从一个或多个本地或远程存储库中请求数据。 -事件-这是确定的操作结果, 可能会或可能不会更改当前状态。

作为图形表示, 可以这样考虑:

Flutter教程:BLoC体系结构的图形表示。

另外, 我们有一个模型目录, 其中包含数据类和产生这些类实例的存储库。

UI开发

使用Flutter创建UI是完全在Dart中完成的, 这与在Android和iOS中进行本机应用程序开发相反, 在Android和iOS中, UI是使用XML方案构建的, 并且与业务逻辑代码库完全分离。我们将基于当前状态(例如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的设计, 它目前不支持嵌套的包含数组的查询。因此, 我们无法检索适当的数据流, 但需要在我们这边执行其他过滤。该解决方案包括检索已登录用户的所有聊天室, 然后搜索还包含所选用户的聊天室:

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仍然缺少一些功能, 以便拥有更简单, 更准确的数据(如嵌套数组)。

如本文开头所提到的, Flutter直到最近才发展到稳定的1.0版本, 并且不仅在框架特性和功能方面, 而且在开发社区和第三方库以及资源。现在就花点时间来熟悉最新的应用程序开发是很有意义的, 因为显然可以保留并加快你的移动开发流程。

Flutter开发人员将不需支付任何额外费用, 就可以瞄准Google新兴的操作系统-紫红色。

相关文章:Dart语言:Java和C#不够清晰时

赞(0)
未经允许不得转载:srcmini » Flutter教程:如何创建你的第一个Flutter应用

评论 抢沙发

评论前必须登录!