Mobile
9 minute read

Flutter Tutorial: How to Create Your First Flutter App

Nemanja has worked for startups and corporations, notably Microsoft. He’s an Agile/Scrum expert and has worked as a team lead and mentor.

What is Flutter?

Flutter is Google’s mobile app development SDK that allows your product to target both Android and iOS platforms simultaneously, without the need to maintain two separate codebases. Furthermore, apps using Flutter can also be compiled to target Google’s upcoming 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. While it can still be regarded as an early and evolving software venture, this article will focus on an already proven concept and demonstrate how to develop a fully-functional messaging app that targets both major mobile platforms using Flutter 1.2 and Firebase.

As can be seen from the chart below, Flutter has been gaining a lot of users in recent months. In 2018, Flutter’s market share doubled and it is on track to surpass React Native in terms of search queries, hence our decision to create a new Flutter tutorial.

Chart comparing Flutter and React users from July through September 2018.

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

Even though effort has been made to allow readers to follow and accomplish this project even if it’s their first attempt at mobile development, a lot of core mobile development concepts that aren’t Flutter-specific are mentioned and used without detailed explanation.

This has been undertaken for article brevity as one of its objectives is for the reader to complete the project in one sitting. Finally, the article assumes you already have your development environment set up, including the required Android Studio plugins and Flutter SDK.

Firebase Set Up

Setting up Firebase is the only thing that we have to do independently for each platform. First of all, make sure you create a new project in the Firebase Dashboard and add Android and iOS applications in the newly generated workspace. The platform will produce two configuration files that you need to download: google-services.json for Android and GoogleService-Info.plist for iOS. Before closing the dashboard, make sure to enable Firebase and Google authentication providers as we’ll use them for user identification. To do this, choose the Authentication item from the menu and then select the Sign-In method tab.

Now you can close the dashboard as the rest of the set up takes place in our codebase. First of all, we need to put the files we downloaded in our project. 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, we need to actually set up the Firebase libraries we’re going to use in the project and hook them up with the configuration files. This is done by specifying the Dart packages (libraries) we’ll be using in our project’s pubspec.yaml file. In the dependencies section of the file, paste the following snippet:

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

The first two aren’t related to Firebase but are going to be frequently used in the project. The last two are, hopefully, self-explanatory.

Finally, we need to configure platform-specific project settings that will enable our authentication flow to complete successfully. On the Android side, we need to add the google-services Gradle plugin to our project-level Gradle configuration. In other words, we need to add the following item to the dependency list in $(FLUTTER_PROJECT_ROOT)/android/build.gradle file:

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

Then we need to apply that plugin by adding this line to the end of $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle:

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

The last thing for this platform is to enlist your Facebook application parameters. 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:

<!-- 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">Toptal Chat</string>
   <string name="facebook_app_id">${YOUR_FACEBOOK_APP_ID}</string>
   <string name="fb_login_protocol_scheme">${YOUR_FACEBOOK_URL}</string>
</resources>

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, you need to add these items to the existing array instead of declaring it again) to $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist file:

<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>

A Word on BLoC Architecture

This architecture standard was described in one of our previous articles, demonstrating the use of BLoC for code sharing in Flutter and AngularDart, so we won’t be explaining it in detail here.

The basic idea behind the main idea is that every screen has the following classes: - view - which is responsible for displaying the current state and delegating user input as events to bloc. - state - which represents “live” data that the user interacts with using the current view. - bloc - which responds to events and updates the state accordingly, optionally requesting data from one or many local or remote repositories. - event - which is a definite action result that may or may not change the current state.

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

Flutter Tutorial: Graphic representation of the BLoC architecture.

Additionally, we have a model directory which contains data classes and repositories that produce instances of these classes.

UI Development

Creating UI using Flutter is done completely in Dart, as opposed to native app development in Android and iOS where the UI is built using the XML scheme and is completely separated from the business logic codebase. We’re going to use relatively simple UI element compositions with different components based on the current state (e.g. isLoading, isEmpty parameters). The UI in Flutter revolves around widgets, or rather the widget tree. Widgets can either be stateless or stateful. When it comes to stateful ones, it’s important to stress that, when setState() is called on a particular widget that is currently displayed (calling it in the constructor or after it’s disposed results in a runtime error), a build and draw pass is scheduled to be performed on next drawing cycle.

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

The rest of the UI classes follow the same patterns but perhaps have different actions and might feature an empty state widget tree in addition to loading state.

Authentication

As you may have guessed, we’ll be using google_sign_in and flutter_facebook_login libraries to authenticate the user by relying on their social network profile. First of all, make sure to import these packages into the file that’s going to handle the login request logic:

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

Now, we’re going to have two independent parts that are going to take care of our authentication flow. The first one is going to initiate either a Facebook or Google sign-in request:

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

The second one is going to be called when we get the profile data from either provider. We’re going to accomplish this by instructing our login handler to listen to 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 implementation will not be posted here, but feel free to take a look at the GitHub repo for full reference.

Flutter Tutorial: How to Build an Instant Messaging App

Finally, we get to the interesting part. As the name implies, the messages should be exchanged as fast as possible, ideally, this should be instant. Luckily, cloud_firestore allows us to interact with Firestore instance and we can use its snapshots() feature to open a data stream that will give us updates in real time. In my opinion, all the chat_repo code is pretty straightforward with the exception of the startChatroomForUsers method. It is responsible for creating a new chat room for two users unless there’s an existing one that contains both users (as we don’t want to have multiple instances of the same user pair) in which case it returns the existing chat room.

However, due to the design of Firestore, it currently doesn’t support nested array-contains queries. So we cannot retrieve the appropriate data stream but need to perform additional filtering on our side. That solution consists of retrieving all the chatrooms for the logged in user and then searching for the one that also contains the selected user:

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

Also, due to similar design constraints, Firebase currently doesn’t support array updates (inserting new element in an existing array field value) with special FieldValue.serverTimestamp() value.

This value indicates to the platform that the field that contains this instead of an actual value should be populated with the actual timestamp on the server at the moment the transaction takes place. Instead, we’re using DateTime.now() at the moment we’re creating our new message serialized object and inserting that object into the chat room messages collection.

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

Wrapping Up

Obviously, the Flutter messaging app we developed is more of a proof-of-concept than a market-ready instant messaging application. As ideas for further development, one might consider introducing end-to-end encryption or rich content (group chats, media attachments, URL parsing). But before all that, one should implement push notifications as they’re pretty much a must-have feature for an instant messaging application, and we’ve moved it out of the scope of this article for the sake of brevity. Additionally, Firestore is still missing a couple of features in order to have simpler and more accurate data-like nested array-contains queries.

As mentioned at the start of the article, Flutter has only recently matured into stable 1.0 release and is going to keep growing, not only when it comes to framework features and capabilities but also when it comes to the development community and third-party libraries and resources. It makes sense to invest your time into getting acquainted with lutter app development now, 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 is used to develop apps that run on Android and iOS and deliver the same user experience. Flutter will also be able to target Fuchsia, the upcoming OS from Google.
Which language does Flutter use?
Flutter development is done in Dart, an open-source programming language developed by Google.
Is Flutter better than React Native?
Both Flutter and React Native deliver the same outcome, a cross-platform app, but they are quite different so it’s hard to simply designate one of them as “better”. Any comparison out of context would have very little value.
Will Flutter replace React Native?
Flutter is owned and developed by Google, which gives it great reach and potential market share. It is currently projected to have bigger development market share by the end of 2019. However, this does not mean it will outright replace React Native at any point.
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 has undergone multiple major updates over the years and currently supports multiple paradigms. This makes it very robust and a good choice for a lot of different product requirements.
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, ahead of time compilation into platform native instruction sets which allows the best performance for delivering production-ready solutions.

Comments

comments powered by Disqus