Let’s Develop a Mobile App in Flutter

Let’s Develop a Mobile App in Flutter

Flutter is a mobile framework for the development of multiplatform native apps, so from one code we will get an app that can run on Android as well as on iOS. It is a similar experiment to Xamarin from Microsoft a few years ago.

Flutter is a mobile framework for the development of multiplatform native apps, so from one code we will get an app that can run on Android as well as on iOS. It is a similar experiment to Xamarin from Microsoft a few years ago.

Let’s Develop a Mobile App in Flutter (1/3)

Flutter developers released a second beta version of the Flutter framework at the beginning of April. It is the second beta version released in just the last month and a half. It seems that after a long development period, there will be a stable version of the framework released quite soon.

Flutter developers released a second beta version of the Flutter framework at the beginning of April. It is the second beta version released in just the last month and a half. It seems that after a long development period, there will be a stable version of the framework released quite soon. In this article we will present the basic interface setting for using Flutter and describe its project structure. We will also use a simple project from the Flutter developers to show how to create an app for Android and iOS that has a UI corresponding to the given platform.

If you are a Flutter guru already (or if you know your way around it), you will not find much new information in this article. But we believe that you will discover some interesting tips and tricks in this series nonetheless.

Let’s get started

The Dart programming language is used for creating Flutter applications. Dart is an object oriented programming language and its syntax is very similar to Kotlin’s, even though it might be a bit more wordy. You can check its specifications and syntax on the official website. We will not discuss Dart much in this article but we will still present a few basic language constructs.

SDKs

We need to have the Flutter and Dart SDKs installed in order to be able to start using Flutter. The installation of the SDK packages is partially dependent on your operating system. We will show only the main commands here, other details and differences can be found on the official Flutter webpage. Because Flutter is in beta, the SDK installation is performed by cloning the official repository, therefore you need to have a git client installed.

git clone -b beta https://github.com/flutter/flutter.git

Then you need to add the path to Flutter to the interface variable PATH. After a correct setup we can call Flutter relatively, thanks to which we ensure the correct function of all other available Flutter tools. Next, we will need the SDK and the relevant development tools for our intended platform. The only development restriction for iOS (or let’s say of the final app assembly) is the necessity of having a device with macOS. Finally, we need to install all the dependent components using the following command. The same command can also be used for the overall configuration check of our interface.

flutter doctor

As Flutter is constantly being developed, the stable SDK version changes on a daily basis. That is why we recommend running the following command every time before you start or continue the development of your Flutter app. It will perform the update of all necessary SDK packages.

flutter upgrade

Editor

We recommend using Android Studio for the development. Flutter developers offer a plugin that will make the creation of your new project a piece of cake. All the following examples will be presented in Android Studio. You can find all the necessary details for the installation of Android Studio and the plugin on the official Flutter website.

Project creation

If you have the Android Studio and the plugin installed, the creation of a new project is a cakewalk. Select the Create New Flutter Project option and go through a simple wizard that will help you set up the basic project configuration.

After the creation, the project contains all the necessary parts for a correct setup and run of the final app as well as a demo project prepared by Flutter developers. In the next part we will use this demo project to show how to create apps for both platforms with ease. In the end, the apps will look as if they are created by a native development process.

Project’s structure

The structure of every Flutter project looks more or less like this:

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.
dependencies:
  ...
  device_info: "^0.2.0"

Flutter application

Assembly of the app

To assemble the final app, we only need to enter one of the following commands, depending on the selected platform. The assembly can be parameterized to a certain degree. The currently supported parameters can be displayed by the last two commands.

flutter build apk          # Build an Android APK file from your app.
flutter build ios          # Build an iOS application bundle (Mac OS X host only).
flutter build apk --help   # Print build parameters
flutter build ios --help   # Print build parameters

As you can see, the final apps created from one code for iOS and Android look alike. After installing the demo app on a specific end device (iPhone X, Pixel 2 XL), it looks like this:

Expanding the demo app

Let’s add more simple logic to the app. After every fifth click on the + button, the app will show a simple dialogue with a query if the user wishes to reset the counter or continue. The user will then, using the dialogue buttons, choose one option. When the OK button is pressed, the counter will reset and the dialogue ends. When pressing the Cancel button, the dialogue will end. At the end of the _incrementCounter method from the _MyHomePageState class, we will add the following code:

if (_counter % 5 == 0) {
  showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return /*TODO create dialog*/
      });
}

Every time the variable _counter is divisible by five, the dialogue will be shown, so everything is set up as we wanted. Now we need to create a function that will create a dialogue with the requested functionality.

Material design dialogue

Let’s add a new method that will create a new alert dialogue in material design to the _MyHomePageState class. The basis is the AlertDialog class, which awaits in the constructor for the configuration of the dialogue being created. An important parameter is the actions field where the individual dialogue buttons are defined. The method for creating the dialogue is as follows:

AlertDialog _createMaterialAlertDialog() => new AlertDialog(
    title: new Text('Reset counter'),
    content: new Text('Do you want to reset counter?'),
    actions: <Widget>[
      new MaterialButton(
        onPressed: () {
          Navigator.pop(context);
        },
        child: new Text('Cancel'),
      ),
      new MaterialButton(
          onPressed: () {
            setState(() {
              _counter = 0;
            });
            Navigator.pop(context);
          },
          child: new Text('OK')),
    ],
  );

iOS design dialogue

The dialogue creation is the same as the preceding one, but instead of the material design component we will use components from the cupertino package. The method for creating the dialogue is as follows:

CupertinoAlertDialog _createCupertinoAlertDialog() => new CupertinoAlertDialog(
    title: new Text('Reset counter'),
    content: new Text('Do you want to reset counter?'),
    actions: <Widget>[
      new CupertinoButton(
        onPressed: () {
          Navigator.pop(context);
        },
        child: new Text('Cancel'),
      ),
      new CupertinoButton(
          onPressed: () {
            setState(() {
              _counter = 0;
            });
            Navigator.pop(context);
          },
          child: new Text('OK')),
    ],
  );

Dialogue based on the target platform

Let’s now combine both concepts together and add a logic that will ensure that the correct dialogue is displayed based on the platform on which the app is running. It is quite simple; we only need to ask what the used platform is.

if (_counter % 5 == 0) {
  showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return Platform.isIOS
            ? _createCupertinoAlertDialog()
            : _createMaterialAlertDialog();
      });
}

As you have probably noticed, the app now contains a lot of redundant code – the source code for both methods for the creation of corresponding dialogues is virtually the same. It differs only in places where we are calling the appropriate class for displaying a specific element. In one of the following articles we will show how to, at least partially, get rid of these redundancies.

Conclusion

In today’s article we did an overview of Flutter, showed the initial steps needed before starting the creation of a Flutter app and described the parts of the Flutter project. We also extended the prepared demo project with the platform dependent dialogue displaying functionality. You can find the whole project at our GitHub.

We will present the basic constructions of the Dart programming language next. We will also show how Flutter actually works from the inside and start creating an app for displaying events, event registrations, and notifications about any event changes.

Let’s Develop a Mobile App in Flutter (2/3)

Language Dart you need to know if you want a Flutter flow. That’s why we’ll have a look at how to write and design in Flutter. We’ll answer the basic question: Why does it work cross-platform? And we’ll also call a native service for the given platform.

The last article presented the Flutter framework: We’ve set up the environment in order to be able to start developing the Flutter application and described the structure of the Flutter app project. We also presented how to create the platform-dependent dialogue.

Today, we’ll introduce Flutter in more technical details. We’ll go over the Dart language in order to understand how to actually write in Flutter. We’ll talk about Flutter from a design perspective and explain why it works cross-platform. And we’ll show how to call a native service for the given platform. Let’s get started.

Language class

Dart is an object-oriented programming language with syntax very similar to today’s modern languages such as Kotlin, C# or Java.

Variables

Dart is a strongly typed language. That means that every variable has its own data type that is determined when the variable is created and it cannot be changed when the program is running. You don’t have to explicitly state the data type when creating a new variable. You can use the keyword var to deduce its type. Even though it’s a strongly typed language, you can create a variable “without any data type” by using the keyword dynamic.

// explicit data type
String firstName = 'Eman';

// data type deduction
var surname = 'Novák';

// variable data type
dynamic company = 'eMan.cz';

//firstName = 10; // compile error
//surname = 10; // compile error
company = 10;

Classes and objects

Everything is an object in Dart. Every object is an instance of a class and all classes inherit from the Object class. The classes consist of class variables and methods (static methods); objects consist of instance variables and methods.

One of the concepts of object-oriented programming is the encapsulation of classes. In Java, for example, you can define several visibility levels, usually by using keywords such as private, protected, or public. Only the private and public visibility, that which is not defined by special keywords, exist in Dart. But while declaring a class, variable etc. for private visibility, it’s sufficient to use the _ prefix in the name of the element. The visibility does not apply to the classes’ level but to the level of the individual app parts – libraries (library).

class _MyPrivateClass {
  String _myPrivateVariable = '...' // private variable in private class
  String myPublicVariable = '...' // public variable in private class
}

class MyPublicClass {
  String _myPrivateVariable = '...' // private variable in public class
  String myPublicVariable = '...' // public variable in public class
}

Note: Each individual Dart file is a library by default. That means that the individual private components are not visible among the files.

An interesting concept of this language is the Named constructors. It’s a classic object constructor but you can define its name. If the constructor is named appropriately, you can deduce how the object is created from its name. In Java, for example, a similar construct could be created using a static method that would create and initialize the actual object.

class Person {
  String firstName;
  String surname;

  Person.fromJson(Map data) {
    firstName = data['first_name'];
    surname = data['surname'];
  }
}

main() {
  var person = new Person.fromJson({'first_name': 'Eman', 'surname': 'von Prag'});
  print(person.firstName); // Eman
  print(person.surname);   // von Prag
}

Functions

Dart also supports classical functions and because everything is an object, so are the functions. The main function is the main() function that is the app’s entry function, therefore every app will start with this function. You can nest the definitions of the individual functions and create, so called, Nested functions. Every nested function gains the context of the function in which it’s nested, it can see its local variables.

bool topLevel = true;

void main() {
  var insideMain = true;

  void nestedFunction() {
    var insideNestedFunction = true;

    void nestednestedFunction() {
      var insideNestedNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideNestedFunction);
      assert(insideNestedNestedFunction);
    }

    //assert(insideNestedNestedFunction) translation error - unknown variable
  }
}

Null-aware operators

Similarly to Kotlin, Dart offers several operators that make notation of the Null Pointer safety code easier.

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

Cascade

The cascade is for calling multiple methods on a single object. In Kotlin, a similar result is obtained by using the apply.

class Person {
  String firstName;
  String surname;
  String company;
}

main() {  
  var person = new Person()
    ..firstName = 'Bob'
    ..surname = 'Clever'
    ..company = 'eMan.cz';
}

Everything is a widget

You’ve probably heard that “everything is a widget” if you’ve read any article about Flutter. And yes, everything is actually a widget. Everything, even the app itself, is a widget. Thanks to this, every UI element can be addressed uniformly. There are two kinds of widgets:

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

Source: https://flutter.io/technical-overview/

Every widget is very small because it manages only the activity it was created for and not any other. For example, the Text widget that displays text manages only the actual display of the text and doesn’t consider its position in connection to the other widgets nor has the ability to gather events (e.g. clicks). If you want it to have any other function, you simply add another widget that supports the function you need. In the end, developing a Flutter app results in putting the individual widgets together in order to get the desired functionality, which reminds us of building with Legos. And everyone loves Legos, right?! 😉

Flutter technology

Flutter does not use any native elements of the target platform. All elements were developed anew by Flutter developers in order to be suitable to the conventions of the target platform. Most of the app’s source code is translated directly into machine code of the target processor (via Android NDK on Android and via LLVM on iOS). That’s why you only need one source code for both platforms. And thanks to this the resulting app performance should be way faster, at least on Android.

You can see the individual framework layers on the diagram below. The dependency of the individual layers is from the top down; each layer depends on the layers below. The green part shows the main components of the Flutter framework. As you can see, Material (Android) and Cupertino (iOS) components are on the same level, making the platform-dependent development quite complicated as we saw in the last article. When looking over the Flutter documentation on its website, you can find a diagram showing the Material component to be dependent on Cupertino. That would solve the above mentioned issue. Unfortunately, when looking into the current source code of Flutter, it seems that the two components are at the same level.

The blue part shows components present in the Flutter core that are responsible for the actual operation of the app. The Skia component is responsible for the rendering of 2D graphics, Dart for compilation of the AOT parts of the code that couldn’t be compiled into the machine code directly, and the Text component manages the rendering of the text.

Edited from source: https://flutter.io/technical-overview/

Native interface

Flutter allows for calling a specific method of the native interface of the given platform using the, so called, Method channel. The Flutter app (client) sends a message with the request to the native part of the app (host). If the host knows the message, it manages the request and returns an answer. For this to be correctly served, both the client and the host have to implement the same message protocol.

Source: https://flutter.io/platform-channels/

As you can see from the diagram, using Method channel you can call any service of the native platform or a service that is offered by third party libraries. It’s also one of the ways to implement a service using the platform-specific services that Flutter doesn’t support. This is useful when, for example, you don’t have a library with the requested service available for Flutter but it exists as a library for Android and iOS.

Let’s see this in action using an example scenario when you want to find out the battery status of your device. As Flutter supports the creation of the platform-specific parts of the source code even in Kotlin for Android parts, and in Swift for iOS parts, we used these languages for our examples.

Client

The client sends a request about the battery status and displays the formatted result in the middle of the screen upon its reception. The message processing is asynchronous so the state of the battery can be displayed only after the result is received. In the example you can see one of the possible methods of processing such asynchronous calls.

An important part is the construction of the MethodChannel object that awaits in the parameter of the constructor for the name of the channel that is being created. On the host side, you can connect the host and the client using the this name. In order for the client and host to communicate, they need to use the same channel as well as send and expect to receive messages of the same type. The invokeMethod() method is used for defining the message type (in the parameter of the method).

The main.dart file in the lib directory should then look like this:

const platform = const MethodChannel('battery');

void main() {
  platform
      .invokeMethod('getBatteryLevel') // sends message with request 'getBatteryLevel'
      .then((result) => 'Battery level at $result %.')
      .catchError((error) => "Failed to get battery level: '${error.message}'.")
      .then((msg) => runApp(new Center(
            child: new Text(
              msg,
              textDirection: TextDirection.ltr,
            ),
          )));
}

Host

It is important that the host registers the handler on the same channel that was defined on the client’s side. After receiving a specific message type, that can be determined by call.method, you can perform the requested operation and save the result in the result object.

The MainActivy.kt file in the android/app/src/main/kotlin/package_name directory should then look like this:

class MainActivity : FlutterActivity() {
    private val CHANNEL = "battery"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)

        MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "getBatteryLevel") {
                val batteryLevel = getBatteryLevel()

                if (batteryLevel != -1) {
                    result.success(batteryLevel)
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }

    private fun getBatteryLevel(): Int {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }
    }
}

The AppDelegate.swift file in the ios/Runner directory should then look like this:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        GeneratedPluginRegistrant.register(with: self);

        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController;
        let batteryChannel = FlutterMethodChannel.init(name: "battery", binaryMessenger: controller);
        batteryChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in
            if (call.method == "getBatteryLevel") {
                let batteryLevel : Int = self.getBatteryLevel();
                if(batteryLevel != -1) {
                    result(batteryLevel);
                } else {
                    result(FlutterError.init(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil));
                }
            } else {
                result(FlutterMethodNotImplemented);
            }
        });

        return super.application(application, didFinishLaunchingWithOptions: launchOptions);
    }

    private func getBatteryLevel() -> Int {
        let device = UIDevice.current;
        device.isBatteryMonitoringEnabled = true;
        if (device.batteryState == UIDeviceBatteryState.unknown) {
            return -1;
        } else {
            return Int(device.batteryLevel * 100);
        }
    }
}

Conclusion

Today we discussed what to use and how to write when designing apps in Flutter. We presented the individual parts of the app and the individual parts of the Flutter framework. And we also tackled the communication with the native platform interface when calling platform-specific services. You can find the whole project at eMan’s GitHub.

Let’s Develop a Mobile App in Flutter (3/3)

We already know how to create dialogues and use Dart. In the last article of this series, we will finally get to the creation of a more complex app. We’ll also discuss how to tackle the support of a platform dependent app. Let’s do this!

In the previous article we had a look into the Flutter framework, introduced Dart, and showed how to call the native interface of the given platform. This article will present possible localization options of the app and we’ll also have a brief look at the creation of a screen layout. Then we’ll incorporate all gained knowledge into our app and, as the final step, we’ll connect it to Firebase. That’ll give us a skeleton of what’s today probably the most used software architecture in mobile development – a mobile app that, in the client’s role, gathers data from a server database.

We make widgets (Abstract Factory pattern)

The first article tackled the creation of a platform dependent dialogue. The presented solution used the corresponding graphic component based on the identification of the target platform. This solution is not ideal as it calls a code with a large number of if-then-else constructions for the identification of the platform and subsequent usage of the specific component. This issue can be fixed, for example, by using the Abstract Factory design pattern.

Abstract Factory in general

The Abstract Factory design pattern is a part of a creational design pattern group. Abstract Factory is used for the creation of similar products and eliminates the usage of new. A group of objects created by a factory is then clearly determined by the relevant factory.

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

(Source: https://en.wikipedia.org/wiki/Abstract_factory_pattern)

To better understand it, picture this example: You’re building a house and want the windows and doors to be from a specific material. You can select from wood, plastic and aluminium, and the only restriction is that you can’t combine various materials.

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

You make your order in a given factory and you know that the supplied products will be from the same material. This way you get rid of asking over and over again: “What material should the door/window be made of?” Instead, you just submit another order to the factory you selected at the beginning.

Abstract Factory in more detail

Our abstract factory will offer an interface for the creation of various graphics and other elements. The creation of the elements itself will be taken care of by two factories, one for Android and another for iOS style. But how do you determine what elements to create in the factory and what elements to create in a different way? We can approach this question from several angles:

  1. creating all graphic components
  2. creating all components that are in the material or cupertino package
  3. creating only the components that exist in both graphic versions

We decided to choose the second option as it’s a nice compromise between the other possibilities. Moreover, if an alternative component is implemented in the other package in the future, adding the component will be way easier. Then the source files should not contain any package:flutter/cupertino.dart or ipackage:flutter/material.dart import in other files than the ones containing the implementation of relevant factories.

When declaring the individual interface methods, it’s recommended to use the “Optional named parameters“ thanks to which the interface change (in a sense of adding a new parameter) influences only the specific factories implementing the given interface and leaves the places where the interface methods are called untouched.

Here is the creation of Scaffold and AppBar components as an example. You can find the whole interface in the widget_factory.dart file.

abstract class WidgetFactory {
  Widget createScaffold({PreferredSizeWidget appBar, Widget body});

  PreferredSizeWidget createAppBar({Widget title});
  ...
}

The implementation of the given factories that ensures the creation of the relevant component based on the requested style:

class _AndroidWidgetFactory implements WidgetFactory {
  @override
  Widget createScaffold({PreferredSizeWidget appBar, Widget body}) {
    return Scaffold(appBar: appBar, body: body);
  }

  @override
  PreferredSizeWidget createAppBar({Widget title}) {
    return AppBar(title: title);
  }
  ...
}

class _IOSWidgetFactory implements WidgetFactory {
  @override
  Widget createScaffold({PreferredSizeWidget appBar, Widget body}) {
    return CupertinoPageScaffold(navigationBar: appBar, child: body);
  }

  @override
  PreferredSizeWidget createAppBar({Widget title}) {
    return CupertinoNavigationBar(middle: title, backgroundColor: Colors.white);
  }
  ...
}

All the necessary classes are ready. We only need to add the logic for the initialization of the factory that will create the elements while the app is running. We used a Singleton design pattern that will make sure that the whole running app will use only one instance of the specific factory. Dart offers the “Factory constructor“ for the creation and a clean usage of Singletons. As we can see from the example, the logic for determining on what system the app is running is only in one place.

abstract class WidgetFactory {
  static WidgetFactory _instance;

  factory WidgetFactory() {
    if (_instance == null) {
      if (Platform.isAndroid) {
        _instance = _AndroidWidgetFactory();
      } else if (Platform.isIOS) {
        _instance = _IOSWidgetFactory();
      } else {
        throw UnsupportedError('Unsupported target platform.');
      }
    }
    return _instance;
  }
  ...
}

// using
WidgetFactory().createScaffold(...);

Note: We could use a static method getInstance() for the creation of the Factory construction for returning the class instance in Java.

Pros and cons

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.
Localization of the app

Today’s standard is that apps are offered in various language mutations in order to attract as wide a spectrum of users as possible. And Flutter is one of them, offering several options on how to get the required properties. Using the intl package for the support of localization is one of the possible options. The package adds the support for localization to any project written in Dart. It establishes a special construction for getting the string’s definitions in the project that ensures the usage of a correct language mutation. Based on this construction and using a special tool, files with corresponding strings are generated for each language mutation that supplement the translation. These are then added to the final app. The second option we’ll present is defining the translation directly in the source code.

We’ll add a new dependency to the pubspec.yaml file that will provide the support for localization.

dependencies:
  flutter_localizations:
    sdk: flutter
  ...

In order to add support for any language to our app, we need to add two classes, as shown in the example below. The _EventsLocalizationsDelegate class is for the loading and storing of the EventsLocalizations object with the relevant, supported localization. Both classes are quite usual and they’ll be similar in every project. The only thing that changes is the values defined in the _localizedValues attribute. It’s a master map where the keys of the first map are marks according to the IANA register.

enum StringId {
  appTitle,
  homeScreenTitle,
  ...
}

class EventsLocalizations {
  static const LocalizationsDelegate<EventsLocalizations> delegate = const _EventsLocalizationsDelegate();

  static const Map<String, Map<StringId, String>> _localizedValues = {
    'en': {
      StringId.appTitle: 'Eman\'s events',
      StringId.homeScreenTitle: 'Events',
      ...
    },
    'cs': {
      StringId.appTitle: 'Eman\'s events',
      StringId.homeScreenTitle: 'Akce',
      ...
    },
    ...
  };

  static final Iterable<Locale> supportedLocales = _localizedValues.keys.map((languageCode) => Locale(languageCode));

  static EventsLocalizations of(BuildContext context) => Localizations.of<EventsLocalizations>(context, EventsLocalizations);

  final Locale _locale;

  EventsLocalizations(this._locale);

  String get appTitle => _localizedValues[_locale.languageCode][StringId.appTitle];
  String get homeScreenTitle => _localizedValues[_locale.languageCode][StringId.homeScreenTitle];
  // or
  String getString(StringId stringId) => _localizedValues[_locale.languageCode][stringId];
}

class _EventsLocalizationsDelegate extends LocalizationsDelegate<EventsLocalizations> {
  const _EventsLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => EventsLocalizations._localizedValues.containsKey(locale.languageCode);

  @override
  Future<EventsLocalizations> load(Locale locale) => SynchronousFuture<EventsLocalizations>(EventsLocalizations(locale));

  @override
  bool shouldReload(LocalizationsDelegate<EventsLocalizations> old) => false;
}

The last thing to do is to specify which language mutations are supported by the app (the supportedLocales parameter of the app’s widget) and register the delegates for all components that’ll be responsible for the correct localization (the localizationsDelegates field of the app’s widget).

class EventsApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: [
        EventsLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: EventsLocalizations.supportedLocales,
      ...
    );
  }
}

Then to get a localized string we’ll use one of the following methods based on what localization class interface we selected.

EventsLocalizations.of(context).appTitle;
// or
EventsLocalizations.of(context).getString(StringId.appTitle);

Note: The current Material Components version does not support Czech, it introduces an error while rendering widgets.

Creating layouts

As previously stated, the layout creation is simply a puzzle. You just put together simple widgets, thus creating a widget that looks and does what was expected of it from the beginning. The following example shows the creation of ListView and its elements and we’ll also take a look at the usage of a few basic layouts. If you’d like to create more complex layouts, take a look at the Flutter webpage for some ideas to get you started.

class EventListState extends State<EventList> {
  final _events = List<Event>();

  @override
  void initState() {
    super.initState();
    _events.addAll(generateWordPairs().take(10).map((pair) => Event(
          pair.asPascalCase,
          DateTime.now(),
          lorem.createParagraph(),
        )));
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        padding: const EdgeInsets.all(20.0),
        itemCount: _events.length * 2,
        itemBuilder: (context, index) {
          if (index.isOdd) {
            return WidgetFactory().createDivider();
          } else {
            return _buildItem(_events[index ~/ 2]);
          }
        });
  }

  Widget _buildItem(Event event) {
    return GestureDetector(
      child: Container(
        decoration: BoxDecoration(color: Colors.transparent),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              padding: const EdgeInsets.only(bottom: 8.0),
              child: Text(
                event.name,
                style: TextStyle(fontSize: 20.0, color: Colors.black),
              ),
            ),
            Text(
              format.format(event.dateTime),
              style: TextStyle(
                fontSize: 16.0,
                color: Colors.grey[500],
              ),
            ),
          ],
        ),
      ),
      onTap: () => _itemClicked(event),
    );
  }
  ...
}

We’ll focus only on the _buildItem method that creates the layout of a single item of the list.

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

You can picture the layout as a tree where every parent nod determines the behaviour or manner of the rendering of its children.

Cloud Firestore database

Cloud Firestore is a Document Store database; it’s the successor to the original Firebase real-time database. We’ll create a Firestore database for our example, with a collection named events containing several documents with the following structure:

name: "...",
date_time: "...",
description: "...",

We won’t go into details of the actual Cloud Firestore database creation and its addition to the project. For more details on this process, please have a look at this guide from Google. After the actual configuration, adding it to the project is a piece of cake. In the previous example, the initState method created a list of events containing random dates that we will now switch for dates from the database. We’ll get the events collection from an instance of the database and all documents that it contains will be mapped by the Event model class. In the following example we regard the events collection as a data stream and, using the listen method, we react to any change of the data. Feel free to try for yourself how the app reacts to you changing the data in the database.

class EventListState extends State<EventList> {
  ...
  @override
  void initState() {
    Firestore.instance
        .collection('events')
        .orderBy('date_time')
        .snapshots()
        .listen((event) => setState(() {
              _events.clear();
              _events.addAll(event.documents.map((snapshot) => Event(
                    snapshot.data['name'],
                    DateTime.parse('${snapshot.data['date_time']}z'),
                    snapshot.data['description'],
                  )));
            }));
  ...
}

Conclusion

In this article we presented how to tackle the issue of keeping the style for a given platform, and we created a support for a multi-language app. Then we created a simple layout and connected the app to the Firestore database. You can find the whole project that we have been very secretly creating here, one part at a time, at our GitHub, where you can also see that the project’s history follows this article.

Flutter Tutorial for Beginners - Build Android and iOS Apps with a Flutter Framework

Build Android and iOS apps with a flutter framework


You’ll learn

  • Better understanding of flutter and it’s basic widgets
  • Develop basic flutter application for android and iOS


Thanks for watching

If you liked this post, share it with all of your programming buddies!

Follow us on Facebook | Twitter

Further reading

Learn Flutter & Dart to Build iOS & Android Apps

Flutter & Dart - The Complete Flutter App Development Course

Dart and Flutter: The Complete Developer’s Guide

Flutter - Advanced Course

Flutter Tutorial - Flight List UI Example In Flutter

Flutter Tutorial for Beginners - Full Tutorial

Using Go Library in Flutter

Parsing JSON in Flutter

Flutter Tutorial - Build iOS & Android App with Google Flutter & Dart

Flutter Tutorial - Build iOS & Android App with Google Flutter & Dart

In this Flutter Tutorial for Beginners, you will learn how to build iOS & Android apps with Google Flutter and Dart

In this post, Flutter Tutorial for Beginners; you will learn how to build iOS & Android apps with Googles Flutter and Dart.

You don't need to learn Android/ Java and iOS/ Swift development to build real native mobile apps!

Flutter - a framework developed by Google - allows you to learn one language (Dart) and build beautiful native mobile apps in no time. Flutter is a SDK providing the tooling to compile that Dart code into native code and it also gives you a rich set of pre-built and pre-styled UI elements (so called widgets) which you can use to compose your user interfaces!

Let's get started in this video!

Take the full course on Udemy at a discount with the following link: http://bit.ly/2O5EaKu

Introduction to Flutter: Building iOS and Android Apps from a Single Codebase

Introduction to Flutter: Building iOS and Android Apps from a Single Codebase

Flutter allows developers to develop both Android and iOS apps using a single codebase. In this tutorial, we will introduce Flutter by building iOS and Android Apps from a Single Codebase.

Flutter allows developers to develop both Android and iOS apps using a single codebase. In this tutorial, we will introduce Flutter by building iOS and Android Apps from a Single Codebase.

Welcome to my first tutorial on Flutter. I have never written any post on cross-platform or hybrid app framework but Flutter has changed this mindset of mine.

Previously, I have developed on React Native, Cordova, Phone Gap, Ionic and now of these really work out for me until I found Flutter along with its huge community of developers and its showcase apps.

What is Flutter?

In a nutshell, it is a multi-layered system, such that higher layers are easier to use and allow you to express a lot with little code and lower layers give you more control at the expense of having to deal with some complexity.

Flutter Framework is written entirely in Dart. Most of the engine is written in C++, with Android-specific parts written in Java, and iOS-specific parts written in Objective-C. Like React Native, Flutter also provides reactive-style views, but Flutter takes a different approach to avoid performance problems caused by the need for a JavaScript bridge by using a compiled programming language, namely Dart.

Dart is compiled “ahead of time” (AOT) into native code for multiple platforms. This allows Flutter to communicate with the platform without going through a JavaScript bridge that does a context switch. It also compiles to native code which in turn improves app startup times.

In Flutter, it is all about Widgets. Widgets are the elements that affect and control the view and interface to an app.

Flutter renders the widget tree and paints it to a platform canvas. This is nice and simple (and fast). It’s Hot-Reload capability allows real-time development experience.

You can read more about Flutter and learn about its goodness here.

Getting Started

Today, we will be building a very simple Flutter app that can be deployed on both iOS & Android called Contactly as we go through this tutorial. This is a very simple Contacts List app which will demonstrate the capabilities of Flutter. Capabilities include:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

The final product of this app should look something like this:

It includes these features:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser
The Flutter’s Project Structure

While you haven’t built any apps using Flutter, let me give you a quick overview of its project structure. Later when you create a Flutter project, you should see a project structure as such:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

I know you can’t wait to try out Flutter. Let’s dive in and set up all the required tools on your machine.

Installing Flutter

At the time of this writing, I’m using the following machine configuration and software version:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

I cannot guarantee that my tutorial will work for every configuration and platform, hence, I will not include configuration troubleshooting here to keep this tutorial short and objective-oriented.

First up, head over to Flutter Installation page to install Flutter. I will skip the steps here as the steps in the document is detailed enough.

Once you run flutter doctor and you got (1~4 checked), you are good to go! It’s not necessary to have Connected Devices checked.

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.0.0, on Mac OS X 10.13.6 17G4015, locale en-SG)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.3)
[✓] iOS toolchain - develop for iOS devices (Xcode 10.1)
[✓] Android Studio (version 3.2)
[✓] Connected device (2 available)

If you have encountered any errors like below, follow the suggested solutions to fix it. For example, if your Mac has not installed with Android Studio, head over to this website to download the software. Just make sure you have the first 4 items checked before moving on.

Creating a new Flutter Project

With Flutter installed, now let’s start to build your first Flutter project.

First, fire up Android Studio and click Start a new Flutter Project.

Next, select Flutter Application and click Next.

Then fill in Project name as contactly, or anything you like. By default, it should show your default path of the Flutter path. In case it doesn’t work for you, navigate and specify your own Flutter SDK path. Optionally, you can change your project location and give a simple description. Then, click Next.

Finally, fill in a Company domain. This will be replicated in your Bundle Identifier (iOS) & Package Name (Android). For my case, I checked both Kotlin & Swift support. Then, click Finish.

Trying out an App on iOS Simulator

Once you started your Flutter Application, some boilerplate code is automatically generated with a sample app that allows you to hit a button and perform some text updates. Before we make any code changes, it is a good checkpoint to try running it on your iOS simulator.

To run the app, find the dropdown list somewhere at the top right that says , click on it and select Open iOS Simulator.

Your last selected simulator hardware will be chosen, which is iPhone XR for my case.

Click Run, which is the green triangle, and the app should open in your simulator. You should be able to interact with the Demo app and push a few buttons!

Building the Main Page

With the demo app running successfully, we are now ready to start building our first Flutter App!

Let’s start by deleting all the code in main.dart. Yes! Press command-a to select the whole code snippet and hit Delete.

Now we will begin to write the code from scratch. First, insert the following line of code to import the material package:

import 'package:flutter/material.dart';

This package is essential for building the UI of the app. To ensure that the app knows what to run after it finishes launching, add the main() method like this:

void main() => runApp(ContactlyApp());

It’s always a good practice to organize files into separate packages and put the constants in a separate. So, let’s create the helper package and the Constants.dart file to place some of our constant values we will be using in this app.

Right-click on the lib folder and then select New > Package. Name the package helpers.

Now we have a separate folder to store our helper classes. To create a new dart file, right-click on helpers and then select New > File. Name it Constants.dart.

In Constants.dart, insert the following code:

import 'package:flutter/material.dart';
 
// Colors
Color appDarkGreyColor = Color.fromRGBO(58, 66, 86, 1.0);
 
// Strings
const appTitle = "Contactly";

Here we import the same material package, so we can use the Color declaration and declare an appTitle to be used app-wide.

Now head back to main.dart and add this import statement after the first import line.

import 'helpers/Constants.dart';

Let’s start building our Main Page by adding these lines of codes:

class ContactlyApp extends StatelessWidget {
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: appTitle,
      theme: new ThemeData(
        primaryColor: appDarkGreyColor,
      ),
    );
  }
 
}

MaterialApp is one of the convenience widgets which allows customisations like adding navigation routes, appBar etc. Setting debugShowCheckedModeBanner to false will get rid of the Red Debug label at the top right. We use our declared appTitle in our constant file here to give it a title. Then, we set the primaryColor.

All the code looks good here and you might be eager to try running it. If you really did, you will get a huge red-colored error screen!

This is because we are not yet ready to paint the canvas. Be Patient!

In most tutorials, they will guide you on building everything into main.dart. But I find that we could make it cleaner by separating each page into separate files, which you will be eventually doing so when building production-ready apps.

Meanwhile, Android Studio should indicate an error in the widget_test.dart file. Since we change the class name from MyApp to ContactlyApp, you should change the following line of code from:

await tester.pumpWidget(MyApp());

to:

await tester.pumpWidget(ContactlyApp());

Building the Login Page

Now let’s go ahead to create a new page called LoginPage.dart and place it under lib. Perform the same ritual of importing material package.

Here we will be creating a Stateless Widget since we don’t need to store any form of data. You can find more details about Stateless VS Stateful here.

Before we go into the code, let’s look at how the login screen should look like:

As you can see, the screen has the following components:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

To implement the screen component, insert the following code. Just copy & paste it first, we will go through them in awhile!

import 'package:flutter/material.dart';
import 'helpers/Constants.dart';
 
// 1
class LoginPage extends StatelessWidget {
 
  // 2
  final _pinCodeController = TextEditingController();
 
  // 3
  @override
  Widget build(BuildContext context) {
     // 3a
    final logo = CircleAvatar(
        backgroundColor: Colors.transparent,
        radius: bigRadius,
        child: appLogo,
    );
 
     // 3b
    final pinCode = TextFormField(
      controller: _pinCodeController,
      keyboardType: TextInputType.phone,
      maxLength: 4,
      maxLines: 1,
      autofocus: true,
      decoration: InputDecoration(
          hintText: pinCodeHintText,
          contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(32.0),
          ),
          hintStyle: TextStyle(
              color: Colors.white
          )
      ),
      style: TextStyle(
        color: Colors.white,
      ),
    );
 
     // 3c
    final loginButton = Padding(
      padding: EdgeInsets.symmetric(vertical: 16.0),
      child: RaisedButton(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(24),
        ),
        onPressed: () {},
        padding: EdgeInsets.all(12),
        color: appGreyColor,
        child: Text(loginButtonText, style: TextStyle(color: Colors.white)),
      ),
    );
 
     // 3d
    return Scaffold(
      backgroundColor: appDarkGreyColor,
      body: Center(
        child: ListView(
          shrinkWrap: true,
          padding: EdgeInsets.only(left: 24.0, right: 24.0),
          children: [
            logo,
            SizedBox(height: bigRadius),
            pinCode,
            SizedBox(height: buttonHeight),
            loginButton
          ],
        ),
      ),
    );
  }
}

And, for the Constants.dart file, please update it like this to add a number of constants that we use in the build method:

import 'package:flutter/material.dart';
 
// Colors
Color appDarkGreyColor = Color.fromRGBO(58, 66, 86, 1.0);
Color appGreyColor = Color.fromRGBO(64, 75, 96, .9);
 
// Strings
const appTitle = "Contactly";
const pinCodeHintText = "Pin Code";
const loginButtonText = "Login";
 
// Images
Image appLogo = Image.asset('assets/images/flutter-logo-round.png');
 
// Sizes
const bigRadius = 66.0;
const buttonHeight = 24.0;

OMG! That’s a huge chunk of code! Yes, but no worries. This is the first time we are really going deep into huge piles of the Dart code. Trust me, after going through these, you will get more familiar with how Flutter works 🙂

I have broken down this large piece of code into 3 major parts so that we can digest them easier:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser
  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

If you run the app now, you will probably end up with an error saying that the image asset cannot be loaded. We know the path is given to load the Image but there are 2 missing pieces: the image itself and the path that we need to include in pubspec.yaml.

First, you can get the logo image I use from here. Then, create a new directory called assets in the root directory, and create a sub-directory called images.

Your image should be placed in root/assets/images.

Then, go to pubspec.yaml and add the following code to inform the app what assets to bundle together during runtime so it can be loaded.

assets:
    - assets/images/flutter-logo-round.png

Please note that you must add the configuration above to the flutter: section like this:

flutter:
  assets:
    - assets/images/flutter-logo-round.png

  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

That was like an Effiel Tower of Codes! UI codes are tough 😭

Before we run the app, we also need to tell our main() to run LoginPage as the home page. So, head back to main.dart and add home: LoginPage() after theme. Your build code should look like this:

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: appTitle,
      theme: new ThemeData(
        primaryColor: appDarkGreyColor,
      ),
      home: LoginPage() // just added
    );
  }
 

Also, you will need to import LoginPage.dart at the very beginning of the file:

import 'LoginPage.dart';

Now run the app! You should see the Login Screen like this:

Cool, right? Let’s continue to build the rest of the screens.

Building Contacts List Page

Now we are warmed up a little, we can go a bit faster. We will now build the main feature of this app, the Contact List page. We will create a new file called HomePage.dart. Once you created the file, make sure you import material package:

import 'package:flutter/material.dart';

Contacts List Page will be a Stateful widget since we need to maintain the state of our contacts data. So add these first few lines of boilerplate codes:

class HomePage extends StatefulWidget {
 
  @override
  _HomePageState createState() {
    return _HomePageState();
  }
 
}
 
class _HomePageState extends State {
 
}

The first class HomePage will be called and used when navigating/presenting the page, while the private class _HomePageState will be called everytime the HomePage is called. This is also the mutable state object which we will maintain as the page get called.

Before we dive into coding again, let’s look at how our contact list screen looks like:

There are many things that we will need to do here:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

Setting up the Routing

Let’s hook up our navigation route between LoginPage & HomePage. Head over to Constants.dart and add these tags:

// Pages
const loginPageTag = 'Login Page';
const homePageTag = 'Home Page';

Then, go to main.dart and add these just before our build function:

  final routes = {
    loginPageTag: (context) => LoginPage(),
    homePageTag: (context) => HomePage(),
  };
 

You will also need to import the HomePage.dart file:

 	
import 'HomePage.dart';

The code above allows us to use tags to associate each individual page. 🙂 Finally, let’s add the routes to our build function just after home.

  Widget build(BuildContext context) {
     ...
     home: LoginPage(),
     routes: routes
    );
  }

We can’t really test this out yet as we have not implemented the UI for our ListView. So, let’s do that first.

Populate JSON data and map to ListView

For this demo, I store all the contact data in a JSON file. You can download the sample JSON file here and create a data folder under assets. Put the records.json file into the folder. Then, update pubspec.yaml with the below asset configuration:

  assets:
    - assets/images/flutter-logo-round.png
    - assets/data/records.json

Now that we have prepared the JSON data, we will need to create:

  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

Record Class to hold a Contact

First, let’s create a new models package under lib and create a new file named Record.dart. You can insert these lines of code into the file:

class Record {
  String name;
  String address;
  String contact;
  String photo;
  String url;
 
  Record({
    this.name,
    this.address,
    this.contact,
    this.photo,
    this.url
  });
 
  factory Record.fromJson(Map json){
    return new Record(
        name: json['name'],
        address: json['address'],
        contact: json ['contact'],
        photo: json['photo'],
        url: json['url']
    );
  }
}

Dart provides factory constructors to support the factory pattern. The factory constructor is able to return values (objects). Here it parses the given JSON string and returns a Record instance, which represents a contact.

RecordList Class to hold the list of Contacts

In the same models package, create another file called RecordList.dart. Then, put in these lines of code:

import 'Record.dart';
 
class RecordList {
  List records = new List();
 
  RecordList({
    this.records
  });
 
  factory RecordList.fromJson(List parsedJson) {
 
    List records = new List();
 
    records = parsedJson.map((i) => Record.fromJson(i)).toList();
 
    return new RecordList(
      records: records,
    );
  }
}

RecordService Class to perform the loading task

Lastly, create another file named RecordService.dart in the same package and insert the following code:

import 'RecordList.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
 
class RecordService {
 
  Future _loadRecordsAsset() async {
    return await rootBundle.loadString('assets/data/records.json');
  }
 
  Future loadRecords() async {
    String jsonString = await _loadRecordsAsset();
    final jsonResponse = json.decode(jsonString);
    RecordList records = new RecordList.fromJson(jsonResponse);
    return records;
  }
 
}

Here, the loadRecords() function parses the records.json file and map it into a RecordList object, holding a list of Record objects. The keyword Future should be new to you if you are unfamiliar with Dart. To perform asynchronous operation in Dart, we use futures. Future objects (futures) represent the results of asynchronous operations.

Implementing the Home Page to list the Contacts

Now let’s use what we have implemented in our HomePage. Open the HomePage.dart and add these import statements at the very beginning:

import 'models/Record.dart';
import 'models/RecordList.dart';
import 'models/RecordService.dart';

Other than listing the contact records, the home page has a search feature that lets users search the contacts. So, first, declare the following variables in the _HomePageState class of the HomePage.dart file:

final TextEditingController _filter = new TextEditingController();
 
RecordList _records = new RecordList();
RecordList _filteredRecords = new RecordList();
 
String _searchText = "";
 
Icon _searchIcon = new Icon(Icons.search);
 
Widget _appBarTitle = new Text(appTitle);

Here is the purpose of each variable:

  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

Since it’s a Stateful widget, we can add some small settings when the state is initialized:

@override
  void initState() {
    super.initState();
 
    _records.records = new List();
    _filteredRecords.records = new List();
 
    _getRecords();
  }
 
  void _getRecords() async {
    RecordList records = await RecordService().loadRecords();
    setState(() {
      for (Record record in records.records) {
        this._records.records.add(record);
        this._filteredRecords.records.add(record);
      }
    });
  } 

In the init state of the home page, we empty our records data and get fresh data from the JSON file. Here we don’t need to really use an Async Call, but it is to introduce its concept and how you could call it if you were to perform a data fetch from a server.

Remember that in our previous section, we return a Scaffold in the build function as the main UI structure. So, continue to insert the following code to create the UI structure:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildBar(context),
      backgroundColor: appDarkGreyColor,
      body: _buildList(context),
      resizeToAvoidBottomPadding: false,
    );
  }

Like most ListView pages we have seen in mobile apps, there is a navigation bar at the top. In the code above, the appBar is the navigation bar. We specify to call _buildBar(context) to generate the bar, however, we haven’t implemented the function yet. So, continue to insert the following code:

Widget _buildBar(BuildContext context) {
    return new AppBar(
      elevation: 0.1,
      backgroundColor: appDarkGreyColor,
      centerTitle: true,
      title: _appBarTitle,
      leading: new IconButton(
            icon: _searchIcon
      )
    );
  }

Next, it’s the body. Again, we haven’t implemented the _buildList(context) function. Continue to add these lines of code:

Widget _buildList(BuildContext context) {
    if (!(_searchText.isEmpty)) {
    _filteredRecords.records = new List();
      for (int i = 0; i < _records.records.length; i++) {
        if (_records.records[i].name.toLowerCase().contains(_searchText.toLowerCase())
            || _records.records[i].address.toLowerCase().contains(_searchText.toLowerCase())) {
          _filteredRecords.records.add(_records.records[i]);
        }
      }
    }
 
    return ListView(
      padding: const EdgeInsets.only(top: 20.0),
      children: this._filteredRecords.records.map((data) => _buildListItem(context, data)).toList(),
    );
  }

Here, we handle the mapping of our RecordList data into our ListVew, and also handle any searches performed.

The final piece of our ListView is the UI for each ListViewItem. Let’s create the _buildListItem function:

Widget _buildListItem(BuildContext context, Record record) {
    return Card(
      key: ValueKey(record.name),
      elevation: 8.0,
      margin: new EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
      child: Container(
        decoration: BoxDecoration(color: Color.fromRGBO(64, 75, 96, .9)),
        child: ListTile(
          contentPadding:
          EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
          leading: Container(
              padding: EdgeInsets.only(right: 12.0),
              decoration: new BoxDecoration(
                  border: new Border(
                      right: new BorderSide(width: 1.0, color: Colors.white24))),
              child: Hero(
                  tag: "avatar_" + record.name,
                  child: CircleAvatar(
                    radius: 32,
                    backgroundImage: NetworkImage(record.photo),
                  )
              )
          ),
          title: Text(
            record.name,
            style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
          ),
          subtitle: Row(
            children: [
              new Flexible(
                  child: new Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        RichText(
                          text: TextSpan(
                            text: record.address,
                            style: TextStyle(color: Colors.white),
                          ),
                          maxLines: 3,
                          softWrap: true,
                        )
                      ]))
            ],
          ),
          trailing:
          Icon(Icons.keyboard_arrow_right, color: Colors.white, size: 30.0),
          onTap: () {},
        ),
      ),
    );
  }

This is a long chunky piece of code. We can again break down and digest this in a simpler way:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser
  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

After implementing all these, it’s almost ready to run the app and test it out! One last thing to make it work is to handle the onPressed event of the login button. Previously, we haven’t specified anything in the implementation. Now go to LoginPage.dart and change the onPressed event of the loginButton variable to the following:

onPressed: () {
          Navigator.of(context).pushNamed(homePageTag);
        },

That’s it! Hit the run button and try to navigate the app from the login page to the home page!

Adding Search Feature

To allow search capability, we have to enable the text editor’s listener. Insert the code below after the _buildListItem method of the HomePage.dart file:

_HomePageState() {
    _filter.addListener(() {
      if (_filter.text.isEmpty) {
        setState(() {
          _searchText = "";
          _resetRecords();
        });
      } else {
        setState(() {
          _searchText = _filter.text;
        });
      }
    });
  }
 
  void _resetRecords() {
    this._filteredRecords.records = new List();
    for (Record record in _records.records) {
      this._filteredRecords.records.add(record);
    }
  }

The search process starts by tapping the search icon. When the search is triggered, we will perform some UI changes:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser

So here is the code you need. Continue to add the following method to handle the search:

void _searchPressed() {
    setState(() {
      if (this._searchIcon.icon == Icons.search) {
        this._searchIcon = new Icon(Icons.close);
        this._appBarTitle = new TextField(
          controller: _filter,
          style: new TextStyle(color: Colors.white),
          decoration: new InputDecoration(
            prefixIcon: new Icon(Icons.search, color: Colors.white),
            fillColor: Colors.white,
            hintText: 'Search by name',
            hintStyle: TextStyle(color: Colors.white),
          ),
        );
      } else {
        this._searchIcon = new Icon(Icons.search);
        this._appBarTitle = new Text(appTitle);
        _filter.clear();
      }
    });
  }

In order to trigger _searchPressed(), add this method in onPressed to _buildBar:

Widget _buildBar(BuildContext context) {
    ...
    icon: _searchIcon,
    onPressed: _searchPressed
    ... 
}  

Now you’re ready to go! Try running the app now and perform some searches! like “Mark”.

Building Contact Details Page

To finish up our Contactly App, let’s build our final Details Page to allow the app to show some more info about a contact. Let’s look at how the final screen looks like first:

It shows the contact’s profile image, its name, address, and phone number. One hidden feature not shown here is to allow user to navigate to an external web browser to view the technology’s website. So let’s get started!

In lib, create a new file called DetailsPage.dart and paste in the following code:

import 'package:flutter/material.dart';
import 'models/Record.dart';
 
// 1
class DetailPage extends StatelessWidget {
  final Record record;
  // 2
  DetailPage({this.record});
 
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text(record.name),
        ),
        body: new ListView(
            children: [
              Hero(
                tag: "avatar_" + record.name,
                child: new Image.network(
                    record.photo
                ),
              ),
              // 3
              GestureDetector(
                   onTap: () {
                     URLLauncher().launchURL(record.url);
                   },
                  child: new Container(
                    padding: const EdgeInsets.all(32.0),
                    child: new Row(
                      children: [
                        // First child in the Row for the name and the
                        // Release date information.
                        new Expanded(
                          // Name and Address are in the same column
                          child: new Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              // Code to create the view for name.
                              new Container(
                                padding: const EdgeInsets.only(bottom: 8.0),
                                child: new Text(
                                  "Name: " + record.name,
                                  style: new TextStyle(
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                              ),
                              // Code to create the view for address.
                              new Text(
                                "Address: " + record.address,
                                style: new TextStyle(
                                  color: Colors.grey[500],
                                ),
                              ),
                            ],
                          ),
                        ),
                        // Icon to indicate the phone number.
                        new Icon(
                          Icons.phone,
                          color: Colors.red[500],
                        ),
                        new Text(' ${record.contact}'),
                      ],
                    ),
                  )
              ),
            ]
        )
    );
  }
}

Here is what this above code does:

  1. TextField & Validations
  2. Button Clicks
  3. Navigations
  4. Image Rendering (Local & Online)
  5. Error Alert Dialog
  6. Scrollable List View
  7. List View Search
  8. JSON File Parsing
  9. JSON to Objects Mapping
  10. Opening External Web Browser
  • First, we have our logo. It is embedded in a Circular Frame by using the CircularAvatar class. It also has an appLogo image.

You should notice a new UI component called GestureDetector. As its name suggests, this widget class is designed to detect touches. When a user touches one of the fields, the app will call URLLauncher().launchURL(record.url) to load the URL in a web browser. This URLLauncher class is not ready yet.

Let’s create a new file called URLLauncher.dart in the helpers directory.

To perform a url launch, we need to install a new package called url-launcher. To do this, we need to update our pubspec.yaml like this:

Here we add a line of configuration to load the url_launcher. After editing, run flutter packages get by hitting the Packages Get button. This is how we install extra packages to increase the capabilities of our app 🙂 Great! You have just gained another skill!

Now go back to URLLauncher.dart, insert the following code to implement the launchURL method:

import 'package:url_launcher/url_launcher.dart';
 
class URLLauncher {
 
  launchURL(String url) async {
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw 'Could not launch $url';
    }
  }
 
}

Head back to the DetailsPage.dart file and import the file we just implemented:

import 'helpers/URLLauncher.dart';

Great! The last step is to enable the navigation from HomePage to DetailsPage. Head back to HomePage.dart and edit the onTap: event of the _buildListItem method like this:

Widget _buildListItem(BuildContext context, Record record) {
            ...
          onTap: () {
            Navigator.push(
                context, MaterialPageRoute(builder: (context) => new DetailPage(record: record)));
          },
        ),
      ),
    );
  }  

Also, don’t forget to import the following file in HomePage.dart:

import 'DetailsPage.dart';

Viola! You are done with the app (not just iOS but Android too)! Run it and enjoy your great work 🙂

Conclusion

You have just gone through a very basic tutorial to get you started in developing on Flutter. In my own opinion, Flutter is developed based on the knowledge of popular mobile apps around where we can easily build UI components in just a few lines of codes. While its scalability is still questionable, we can see that Google and it’s community is investing a lot in this framework, and we could possibly forsee a bright future ahead for Flutter, striving past React Native.

You can download the finished project here.