Flutter Tutorial: How to Create Your First Flutter App

What is Flutter?

Flutter is the Google Mobile App Development SDK that enables your product to simultaneously target both Android and iOS platforms without the need for two distinct code bases. In addition, it is also possible to compile applications using Flutter to target the upcoming Fuchsia operating system from Google.

Recently, Flutter struck a significant milestone-version 1.0 stable. The release took place at the Flutter Live event in London on December 5, 2018. While it can still be considered as an early and developing software project, this article will concentrate on an already established idea and show how to create a fully functional messaging app that uses Flutter 1.2 and Firebase to target both significant mobile platforms.

As can be seen from the graph below, in the latest months, Flutter has gained a lot of customers. In 2018, the market share of Flutter increased, and in terms of search queries, it is on track to surpass React Native, hence our choice to produce a fresh tutorial for Flutter.

Essentials

Although efforts have been made to enable readers to follow and achieve this project even though it is their first attempt at mobile development, many key mobile development ideas that are not specific to Flutter are discussed and used without comprehensive explanation.

This was done for brevity of paper as one of its goals is for the reader to finish the project in a single session. Finally, the paper assumes that your development environment, including the necessary Android Studio plugins and the Flutter SDK, has already been set up.

Firebase Set Up

The only thing we have to do separately for each platform is to set up Firebase. First, ensure that you develop a fresh project in the Firebase Dashboard and add Android and iOS apps to the freshly created workspace. The platform produces two settings files you need to download: for Android, google-services.json and for iOS, GoogleService-Info.plist. Before closing the dashboard, ensure that authentication services for Firebase and Google are enabled as we will use them to identify users. To do this, select the menu item Authentication and then select the Method Sign-In tab.

Now, as the remainder of the setup takes place in our codebase, you can close the dashboard. First of all, we need to put in our project the documents that we downloaded. The google-services.json file should be placed in the $(FLUTTER PROJECT ROOT)/android / app folder and the $(FLUTTER PROJECT ROOT)/ios / Runner directory should be placed in GoogleService-Info.plist. Next, we need to set up the Firebase libraries that we will be using in the project and connect them to the settings documents. This is achieved by specifying the packages of Dart (libraries) that we will use in the pubspec.yaml file of our project. Paste the following snippet in the file’s dependencies section:

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

The first two are not Firebase linked but will be used commonly in the project. Hopefully, the last two are self-explanatory.

Finally, we need to configure platform-specific project configurations to allow the successful completion of our authentication flow. On the Android side, we need to add the Gradle plugin to our Gradle setup at project level. In other words, in the $(FLUTTER PROJECT ROOT)/android / build.gradle file, we must add the following item to the dependency list:

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

Then we need to add this line to the end of the plugin

$(FLUTTER_PROJECT_ROOT)/android/app/build.gradle:
apply plugin: 'com.google.gms.google-services'

The last thing about this platform is to enlist the parameters of your Facebook application. Editing these two files is what we are looking for here-

$(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 is iOS time. Fortunately, in this situation, we only need to alter one file. Add the following values to $(FLUTTER PROJECT)ROOT / ios / Runner / Info.plist file (note that CFBundleURLTypes item may already be in the list; in that event, add these items to the current set instead of declaring it again):

<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 about the architecture of BLoC

This architecture standard has been defined in one of our past papers, showing the use of BLoC to share code in Flutter and AngularDart, so we’re not going to explain it in detail here.

The fundamental concept behind the primary concept is that each screen has the following classes:-view-which shows the present state and delegates user input as occurrences to block. State-representing “live” information interacting with the customer using the present perspective. Block-responding to occurrences and updating the state accordingly, optionally requesting information from one or several local or remote repositories. The event-a definite outcome of the action that may or may not alter the present state.

It can be believed of as this as a visual depiction:

We also have a model folder containing information classes and repositories producing cases of these classes.

UI Development

Unlike indigenous app creation in Android and iOS where the UI is constructed using the XML system and is totally separated from the business logic codebase, creating UI using Flutter is performed entirely in Dart. We will use comparatively easy compositions of UI elements depending on the present state with distinct parts (e.g. isLoading, isEmpty parameters). The Flutter UI is about widgets, or rather the tree of the widget. Widgets can be stateful or stateless. When it comes to stateful ones, it is important to stress that a build and draw pass is scheduled to be executed on the next drawing cycle when setState) (is called on a particular widget that is currently displayed (calling it in the constructor or after it has been disposed of resulting in a runtime error).

For brevity, only one of the UI classes (view) will be shown 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 remainder of the UI classes follow the same models but may have distinct behavior and may have an empty status widget tree as well as loading status.

Authentication

We will use google sign in and flutter facebook login libraries, as you might have guessed, to authenticate the user by relying on their profile on the social network. First, ensure that these packages are imported into the file that will manage the login request logic:

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

Now, we will have two separate components that will take care of our authentication flow. The first will initiate either a sign-in application from Facebook or 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));
    }
}

The second one will be called when either supplier receives the profile information. We will do this by instructing our login handler to listen to the firebase auth flow 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));
   });
 }
}

Flutter Tutorial: How to create an app for instant messaging

We’re finally getting to the exciting portion. The messages should be exchanged as quickly as possible, as the name indicates, ideally this should be instantaneous. Fortunately, cloud firestore enables us to communicate with Firestore example and we can use its snapshots (function to open a data stream that will provide us with real-time updates. )

In my view, with the exception of the startChatroomForUsers technique, all the chat repo code is quite simple. It is responsible for creating a new chat space for two users unless there is a current one with both users (because we don’t want numerous cases of the same user pair) in which case it returns the existing chat room.

However, it presently does not support nested array-contains queries due to Firestore’s layout. So we can’t get the right data stream, but on our side, we need to do extra filtering. This alternative comprises of finding all the chatrooms for the user logged in and then looking for the one that also includes the user chosen:

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 also fails to help array updates (inserting a fresh element in a current array field value) with a special FieldValue.server timestamp) (value owing to comparable design limitations.

This value indicates to the platform that at the moment the transaction takes place, the field containing this instead of an actual value should be filled in with the actual time mark on the server. Instead, at the time we are creating our fresh message serialized object, we are using DateTime.now) (and inserting that object into the set of chat room texts.

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 have created is more of a proof of concept than an instant messaging application ready for the market. One might consider introducing end-to-end encryption or wealthy content (group chats, media attachments, parsing of URLs) as ideas for further growth. But first of all, one should implement push notifications as they are almost a must-have feature for an instant messaging application, and for the sake of brevity, we have moved it out of the scope of this article. In addition, Firestore still lacks a few characteristics to have data-like nested array-containing queries that are easier and more precise.

As stated at the beginning of the article, Flutter has only lately developed into a stable 1.0 release and will continue to grow, not only in terms of framework characteristics and capacities but also in terms of development society and third-party libraries and resources. It makes sense to invest your time in getting to know the growth of the lutter app now, as staying and speeding up your mobile development process is obviously here.

Obviously, in today’s scenario, the requirement of coders is very high and so Codersera has taken an initiative of providing the best coders who have very high coding experience.

Hire Coders Now!

Leave a Comment

Your email address will not be published. Required fields are marked *