作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Nemanja Stošić's profile image

Nemanja Stošić

Nemanja曾为创业公司和大公司工作,尤其是微软. 他是敏捷/Scrum专家,担任过团队领导和导师.

Previously At

Microsoft
Share

What is Flutter?

Flutter是Google的移动应用开发SDK,允许你的产品同时瞄准Android和iOS平台, without the need to maintain two separate codebases. 此外,使用Flutter的应用程序也可以针对谷歌即将推出的应用程序进行编译 Fuchsia operating system.

Flutter recently hit a major milestone - stable version 1.0. The release took place in London, December 5th, 2018, at the Flutter Live event. 虽然它仍然可以被视为一个早期的和不断发展的软件企业, 本文将重点介绍一个已经得到验证的概念,并演示如何开发一个功能齐全的消息传递应用程序,该应用程序针对两个主要的移动平台 Flutter 1.2 and Firebase.

如下图所示,Flutter在最近几个月获得了大量用户. In 2018, Flutter的市场份额翻了一番,在搜索查询方面也有望超过React Native, hence our decision to create a new Flutter tutorial.

图表比较了2018年7月至9月的Flutter和React用户.

Note: This article focuses only on certain bits of the implementation. Full source code reference for the project can be found in this GitHub repo.

Prerequisites

即使这是读者第一次尝试移动开发,我们也会努力让读者跟随并完成这个项目, 许多核心的手机开发概念并不是专门针对《欧博体育app下载》的,但却没有给出详细的解释.

这样做是为了文章的简洁,因为它的目标之一是让读者一次完成这个项目. Finally, 本文假设您已经设置好了开发环境, including the required Android Studio plugins and Flutter SDK.

Firebase Set Up

设置Firebase是我们必须为每个平台独立做的唯一事情. First of all, make sure you create a new project in the Firebase Dashboard 并在新生成的工作区中添加Android和iOS应用程序. 该平台将生成您需要下载的两个配置文件: google-services.json for Android and GoogleService-Info.plist for iOS. Before closing the dashboard, 请确保启用Firebase和Google身份验证提供商,因为我们将使用它们来进行用户身份验证. 为此,请从菜单中选择Authentication项,然后选择Sign-In方法选项卡.

现在您可以关闭仪表板,因为其余的设置将在代码库中进行. 首先,我们需要把下载的文件放到我们的项目中. The google-services.json file should be placed in the $(FLUTTER_PROJECT_ROOT)/android/app folder and GoogleService-Info.plist should be placed in the $(FLUTTER_PROJECT_ROOT)/ios/Runner directory. Next, 我们需要实际设置项目中将要使用的Firebase库,并将它们与配置文件连接起来. 这是通过指定我们将在项目中使用的Dart包(库)来完成的 pubspec.yaml file. 在文件的依赖项部分,粘贴以下代码片段:

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

前两个与Firebase无关,但将在项目中频繁使用. The last two are, hopefully, self-explanatory.

Finally, 我们需要配置特定于平台的项目设置,使我们的身份验证流能够成功完成. On the Android side, 我们需要将google-services Gradle插件添加到我们的项目级Gradle配置中. 换句话说,我们需要将以下项添加到中的依赖项列表中 $(FLUTTER_PROJECT_ROOT)/android/build.gradle file:

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应用程序参数. What we are looking for here is editing these two files - $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml and $(FLUTTER_PROJECT_ROOT)/android/app/src/main/res/values/strings.xml:


 

 
        
        
                
                    
                    
                    
                    
                
        
 
                                                                           
                                                                           

    

 


   Toptal Chat
   ${YOUR_FACEBOOK_APP_ID}
   ${YOUR_FACEBOOK_URL}

Now it’s time for iOS. Luckily, we only need to change one file in this case. Add the following values (note that CFBundleURLTypes item may already exist in the list; in that case, 您需要将这些项添加到现有数组中,而不是再次声明它 $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist file:

CFBundleURLTypes

  
     CFBundleURLSchemes
     
        ${YOUR_FACEBOOK_URL}
     
  
  
     CFBundleTypeRole
     Editor
     CFBundleURLSchemes
     
        ${YOUR_REVERSED_GOOGLE_WEB_CLIENT_ID}
     
  

FacebookAppID
${YOUR_FACEBOOK_APP_ID}
FacebookDisplayName
${YOUR_FACEBOOK_APP_NAME}
LSApplicationQueriesSchemes

  fbapi
  fb-messenger-share-api
  fbauth2
  fbshareextension

A Word on BLoC Architecture

在我们之前的一篇文章中描述了这个体系结构标准,演示了 BLoC for code sharing in Flutter and AngularDart, so we won’t be explaining it in detail here.

主要思想背后的基本思想是,每个屏幕都有以下类: - view -它负责显示当前状态并将用户输入作为事件委托给bloc. - state -它表示用户使用当前视图与之交互的“实时”数据. - bloc - which responds to events and updates the state accordingly, 可选地从一个或多个本地或远程存储库请求数据. - event -这是一个确定的动作结果,可能会也可能不会改变当前状态.

As a graphic representation, it can be thought of like this:

Flutter教程:BLoC架构的图形表示.

Additionally, we have a model 目录,其中包含数据类和生成这些类实例的存储库.

UI Development

Creating UI using Flutter is done completely in Dart, 与Android和iOS的原生应用开发相反,Android和iOS的UI是使用XML模式构建的,与业务逻辑代码库完全分离. 我们将使用相对简单的UI元素组合,根据当前状态使用不同的组件.g. isLoading, isEmpty parameters). Flutter中的UI围绕着小部件,或者说是小部件树. Widgets can either be stateless or stateful. 当涉及到有状态时,强调这一点很重要 setState() 在当前显示的特定小部件上调用(在构造函数中调用它或在它被处置后调用它会导致运行时错误)。, 计划在下一个绘图周期中执行构建和绘制阶段.

For brevity, we’ll only show one of the UI (view) classes here:

class LoginScreen extends StatefulWidget {
 LoginScreen({Key key}) : super(key: key);
 
 @override
 State createState() => _LoginState();
}
 
class _LoginState extends State {
 final _bloc = LoginBloc();
 
 @override
 Widget build(BuildContext context) {
   return BlocProvider(
     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(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: [
                   ButtonTheme(
                     minWidth: 256.0,
                     height: 32.0,
                     child: RaisedButton(
                       onPressed: () => BlocProvider.of(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(context).onLoginFacebook(this),
                       child: Text(
                         "Login with Facebook",
                         style: TextStyle(color: Colors.white),
                       ),
                       color: Colors.blueAccent,
                     ),
                   ),
                 ],
               ),
             );
           }
         }),
   );
 }
 
 void navigateToMain() {
     NavigationHelper.navigateToMain(widgetState.context);
 }
}

其余的UI类遵循相同的模式,但可能具有不同的操作,并且除了加载状态之外可能还具有一个空状态小部件树.

Authentication

As you may have guessed, we’ll be using google_sign_in and flutter_facebook_login 图书馆通过依赖用户的社会网络配置文件来验证用户. First of all, 确保将这些包导入到将要处理登录请求逻辑的文件中:

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

Now, 我们将有两个独立的部分来处理我们的身份验证流. 第一个将启动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 stream:

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));
   });
 }
}

The UserRepo and LoginRepo 实现将不会张贴在这里,但请随意看看 the GitHub repo for full reference.

Flutter Tutorial: How to Build an Instant Messaging App

Finally, we get to the interesting part. 顾名思义,应该尽可能快地交换消息,理想情况下应该是这样 instant. Luckily, cloud_firestore 允许我们与Firestore实例进行交互,我们可以使用它的 snapshots() 功能打开数据流,将给我们实时更新. In my opinion, all the chat_repo code is pretty straightforward with the exception of the startChatroomForUsers method. 它负责为两个用户创建一个新的聊天室,除非存在一个包含两个用户的现有聊天室(因为我们不希望有同一用户对的多个实例),在这种情况下,它返回现有的聊天室.

然而,由于Firestore的设计,它目前不支持嵌套 array-contains queries. 因此,我们无法检索适当的数据流,而需要在我们这边执行额外的过滤. 该解决方案包括检索登录用户的所有聊天室,然后搜索也包含所选用户的聊天室:

Future startChatroomForUsers(List 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 chatroomMap = Map();
   chatroomMap["messages"] = List(0);
   List participants = List(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);
 }
}

Also, due to similar design constraints, Firebase目前不支持数组更新(在现有数组字段值中插入新元素) FieldValue.serverTimestamp() value.

该值向平台表明,在事务发生时,应该用服务器上的实际时间戳填充包含该值(而不是实际值)的字段. Instead, we’re using DateTime.now() 目前,我们正在创建新的消息序列化对象,并将该对象插入聊天室消息集合.

Future 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 serializedMessage = {
     "author" : authorRef,
     "timestamp" : DateTime.now(),
     "value" : message
   };
   chatroomRef.updateData({
     "messages" : FieldValue.arrayUnion([serializedMessage])
   });
   return true;
 } catch (e) {
   print(e.toString());
   return false;
 }
}

Wrapping Up

Obviously, 我们开发的Flutter消息应用程序更多的是一个概念验证,而不是一个市场就绪的即时消息应用程序. As ideas for further development, 可以考虑引入端到端加密或富内容(群聊), media attachments, URL parsing). But before all that, 应该实现推送通知,因为它们几乎是即时消息应用程序的必备功能, 为了简洁起见,我们把它移出了本文的范围. Additionally, Firestore仍然缺少一些功能,以便拥有更简单和更准确的数据(如嵌套) array-contains queries.

正如文章开头提到的,Flutter最近才成熟到stable 1.0 release and is going to keep growing, 不仅在框架特性和功能方面,在开发社区和第三方库和资源方面也是如此. 现在投入时间来熟悉Flutter应用程序开发是有意义的, as it’s clearly here to stay and speed up your mobile development process.

At no additional expense, Flutter developers will also be ready to target Google’s emerging OS–Fuchsia.

Understanding the basics

  • What is Flutter used for?

    Flutter用于开发在Android和iOS上运行的应用程序,并提供相同的用户体验. Flutter还可以瞄准谷歌即将推出的操作系统Fuchsia.

  • Which language does Flutter use?

    Flutter的开发是在Dart中完成的,Dart是一种由Google开发的开源编程语言.

  • Is Flutter better than React Native?

    Both Flutter and React Native deliver the same outcome, a cross-platform app, 但是它们是完全不同的,所以很难简单地指出其中一个是“更好的”。. Any comparison out of context would have very little value.

  • Will Flutter replace React Native?

    Flutter是由谷歌拥有和开发的,这给了它很大的影响力和潜在的市场份额. 目前预计,到2019年底,中国将拥有更大的开发市场份额. 然而,这并不意味着它会在任何时候完全取代React Native.

  • What is Dart programming language used for?

    Dart is a Google developed, open-source, scalable programming language, with robust libraries and runtimes, for building web, server, and mobile apps.

  • Is Dart a good language?

    Dart多年来经历了多次重大更新,目前支持多种范式. 这使得它非常健壮,对于许多不同的产品需求来说是一个很好的选择.

  • Is Dart a compiled language?

    Dart is quite unique in this regard. It supports transpilation into JavaScript, a standalone VM which offers just-in-time compilation and, finally, 提前编译成平台本地指令集,从而为交付生产就绪的解决方案提供最佳性能.

Hire a Toptal expert on this topic.
Hire Now
Nemanja Stošić's profile image
Nemanja Stošić

Located in Vancouver, BC, Canada

Member since May 25, 2018

About the author

Nemanja曾为创业公司和大公司工作,尤其是微软. 他是敏捷/Scrum专家,担任过团队领导和导师.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Microsoft

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.