loading...

A first look at Flutter

svenhennessen profile image Sven Hennessen ・8 min read

This article was originally published on hennessen.net on December 22nd, 2019

Cross-platform app development (read "iOS and Android from the same code-base") is the wet dream of many teams and companies for a long time. I've spent quite some time with projects based on HTML5 + Cordova or Xamarin.iOS / .Android (not Forms), so I have a bit of experience in the area and try to keep up with the trends.

Flutter is in everybody's mouth these days when talking cross-platform app development. Basically, Flutter is Google's approach to the cross-platform topic, based on their own programming language, Dart with a declarative UI SDK. I won't repeat the details of the language or SDK here or its features here. Head over to flutter.dev if you're interested in more.

Apple introduced their own, declarative UI development framework, called SwiftUI, when releasing iOS 13. Thinking declarative UI code often is superior to other approaches (see XAML for WPF, XAML for Xamarin.Forms or Google's argument for the approach in the Flutter docs, ), I gave it quick shot, but became frustrated quickly for all the bugs in the first releases. Hence, it was time to give Flutter a shot for my next iOS side project.

This post shows the key aspects of that app and how I implemented them.

The App - Spiramentum 2

The app is a very simple meditation app. The main features I wanted to have are:

  • The ability to configure a timeframe for how much time I want to spend
  • A timer display, for checking the time that has past
  • A push notification at the end of the time
  • Store the time spend in Apple's HealthKit as mindful time spent

Besides playing around with another SDK, this app addresses a particular problem for me: All existing mindfulness or meditations apps in the app store, which I've checked, require a registration/login or event subscription.

One question you might have is: Why is the app called "Spiramentum 2"? Have a look at the app project README for the answer.

Widgets

To follow the description in this blog, it is important to understand an important thing in Flutter: Everything is a "widget". With that I mean your screens/pages, controls, layouts, spaces between controls, paddings, etc. Everything inherits from the Widget base class and shares the same life cycle. Flutter provides a variety of widgets as part of the SDK.

The main screen

The single screen of the app is implemented as a stateful flutter widget called MyHomePage and the corresponding MyHomePageState. We need state to maintain stuff, like the selected time to run the timer for or its start timestamp. In addition, other technically required components like a controller for animations are maintained here as well.

Everything is setup in the void initState() method and cleaned up, if necessary in the void dispose() method, which are both inherited from State<MyHomePage>. Another inherited method is Widget build(BuildContext context), which controls what is rendered to the UI. Here is the structure for our UI:

@override
  Widget build(BuildContext context) {
    final titleText = Padding(
        child: Text(
            // A title label to tell the user what to do
        )
    );

    final durationPicker = CupertinoPicker(
        // A picker to the allow selection for different timeframees
    );

    final pickerTransition = SizeTransition(
        // A transition wrapper for the picker
    );

    final timerTextTransition = SizeTransition(
        child: Text(
          // A label for the timer, wrapped in a transition
        ),
    );

    final startStopButton = CupertinoButton(
      // Button to start/stop the timer
    );

    // Finally the page scaffold putting everything together
    return CupertinoPageScaffold(
        child: Column(
          children: <Widget>[
            Spacer(),
            titleText,
            Spacer(),
            pickerTransition,
            timerTextTransition,
            Spacer(),
            startStopButton,
            Spacer()
          ],
        ),
    );
  }

Since the app is targeted for iOS only, only Flutter's Cuptertino* controls (see the flutter docs) are being used.

The two SizeTransitions are used to switch between the timer and the picker, depending on the state of the timer. I will come back to that later.

Interaction and State

Interaction with the UI, background processing, what is executed on the UI thread and what is not have been problematic areas in past SDKs and led to errors in projects. Hence I was curious to see how Flutter solves this and luckily the answer is "very simplistic".

Explicit actions like our button to start and stop the timer, simply have a callback action.

Button(
    onPressed(): {
        // do stuff
    }
)

Updates to the UI are automatically triggered, each time state on a stateful Widget. State updates are triggered through the void setState(VoidCallback fn) method on the widget, which might sound familiar if you've worked with React before.

_myAction() {
    // Update UI
    setState(() {
      myStateProperty = newValue;
    });
}

By the way, the underscore before the method declaration marks the method as private in Dart as you would know it from other languages like C# or Java.

So far so good, but what about long running tasks which should not block my UI? Well, Dart supports async/await like other languages/frameworks do today. Async methods return their value wrapped into the Future type, which corresponds to the Task in .NET or a Promise in JavaScript.

I think, this is basically everything you need without having to worry too much about threading issues or similar. Oh and for our app's timer, there is the Timer class from the dart:async library, which does the heavy lifting (scheduling and threading) for us.

Animation

One thing Flutter surprised me with, was the simple animation API and diverse standard widgets which already allow all sorts of animations. Our simple app uses the SizeTransformation widget, which can be easily animated using a AnimationController.


// do this in initState
var _animationController = AnimationController(
        duration: const Duration(milliseconds: 400), vsync: this);
var _pickerAnimation = 
        Tween<double>(begin: 1, end: 0).animate(_animationController);
var _counterLabelAnimation =
        Tween<double>(begin: 0, end: 1).animate(_animationController);

// and this in build
final pickerTransition = SizeTransition(
    sizeFactor: _pickerAnimation,
    child: ...
);

final timerTextTransition = SizeTransition(
    sizeFactor: _counterLabelAnimation,
    child: ...
);

Having the two transitions rendered right above each other, it is easy to hide one and show the other with _animationController.forward() and _animationController.reverse(). This is how the picker is replaced by the label while the timer is running and vice versa.

Native Plugin

To achieve the functionality I described in the beginning, we need direct access to the HealthKit and UNUserNotification APIs of iOS. Well, there is no native API in Dart/Flutter, hence the answer was to provide a native iOS plugin.

The API is straight-forward and string-based similar to native plugin APIs, e.g. in Cordova.

These are the two classes provided on the Flutter side to wrap the API calls:

import 'package:flutter/services.dart';
import 'dart:async';

class NotificationService {

  static const platform = const MethodChannel('de.sventropy/notification-service');

  Future<void> showNotification(String title, String message) async {
    await platform.invokeMethod('showNotification', [title, message]);
    print("Triggered notification with text $message");
  }
}

class MindfulStore {

  static const platform = const MethodChannel('de.sventropy/mindfulness-minutes');

  Future<void> storeMindfulMinutes(int minutes) async {
      await platform.invokeMethod('storeMindfulMinutes',minutes);
      print("$minutes stored");
  }
}

In the iOS Runner project, this is the code provided to receive the plugin call and call the native API

override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

    let flutterViewController = self.window.rootViewController as! FlutterViewController // provided via FlutterAppDelegate base class
    let storeMindfulMinutesChannel = FlutterMethodChannel(name: "de.sventropy/mindfulness-minutes", 
        binaryMessengerflutterViewController.binaryMessenger)
    let showNotificationChannel = FlutterMethodChannel(name: "de.sventropy/notification-service", 
        binaryMessengerflutterViewController.binaryMessenger)

    // ensure permissions for notifications and HealthKit

    storeMindfulMinutesChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
        guard call.method == "storeMindfulMinutes" else {
            result(FlutterMethodNotImplemented)
            return
        }
        let minutes = call.arguments as! Int32
        self.storeMindfulMinutes(minutes: minutes, result: result)
    }

    showNotificationChannel.setMethodCallHandler {
        (call: FlutterMethodCall, result: @escaping FlutterResult) in
        guard call.method == "showNotification" else {
            result(FlutterMethodNotImplemented)
            return
        }
        let args = call.arguments as! Array<Any>
        let title = args[0] as! String
        let message = args[1] as! String
        self.sendNotification(title: title, message: message, result: result)
    }

    // register plugins with platform

    func storeMindfulMinutes(minutes: Int32, result: @escaping FlutterResult) {

        let endDate = Date()
        let startDate = Calendar.current.date(byAdding: .minute, value: Int(minutes * -1), to: endDate)!
        let mindfulSessionTime = HKCategorySample(type: HKObjectType.categoryType(forIdentifier: .mindfulSession)! , 
            value: HKCategoryValue.notApplicable.rawValue, start: startDate , end: endDate)

        self.healthStore!.save(mindfulSessionTime, withCompletion: { (success, error) in
            if !success {
                result(FlutterError(code: "UNAVAILABLE",
                                    message: "Error storing mindfulness time",
                                    details: "\(String(describing: error?.localizedDescription))"))
            } else {
                result(true)
            }
        })
    }

    func sendNotification(title: String, message: String, result: @escaping FlutterResult) {
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = message
        content.badge = 1
        content.sound = UNNotificationSound.default()

        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1,
                                                        repeats: false)

        let requestIdentifier = "de.sventropy.spiramentum2.notification"
        let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger)

        UNUserNotificationCenter.current().add(
            request,
            withCompletionHandler: { (error) in
                if error != nil {
                    result(FlutterError(...))
                } else {
                    result(true)
                }
        })
    }
}

One thing to point out: Both plugins are "write-only" since only data is sent to the native platform but none is returned. However, it is important to call the result(...) method anyway, otherwise the asynchronous plugin call will not return.

So basically, this concept is nothing new, works as intended, but will never have the flexibility of a native app with all these APIs at hand without a plugin wrapper.

Community

What I liked looking around for resources on Flutter was the community work around the SDK. I'd like to point out three examples:

  • The "Widget of the Week" videos on the Flutter YouTube channel, where Google presents an existing or new Widget per week, by example
  • The Awesome Flutter community project on GitHub with a collection of other projects, concepts and patterns
  • The Flutter i18n plugin for VSCode built by a friend of mine

My gut feeling is, that there is simply more going on around after a short live of Flutter of ~2.5 years compared to other SDKs/Frameworks. I've witnessed the rise and life of Xamarin from the first line, which at least felt a little less noisy than the Flutter hype.

Conclusion

I like Flutter. Most of all for its accessible declarative API, simple concepts and its easy to learn language, Dart. If you've worked with Swift, you'll know that that not all languages are like that.

Update 2019-12-23: This conclusion would not be complete without stating that Flutter, similar to other cross-platform approaches with their own runtime and API can never be as performing and up-to-date (in terms of new features) as the native SDK is. It still is fun and easy to use ¯\(ツ)/¯.

That's it. The entire project is available on GitHub, so feel happy to browse the code.

Feedback? I am happy to hear it @svenhennessen on Twitter.

Posted on by:

svenhennessen profile

Sven Hennessen

@svenhennessen

Freelance software engineer with a whole lot of experience in mobile and .NET applications. Helping web and cloud projects as well these days.

Discussion

pic
Editor guide
 
Sloan, the sloth mascot Comment marked as low quality/non-constructive by the community View code of conduct

Hello, I'm IOS developer. Recently, when developing applications using swift, I found that there are few caches written in pure swift. So I wrote a cache - swift cache, which is a lightweight general IOS cache library using swift 5. If you are using swift for development and need to use cache, maybe you can try swiftlycache, maybe you will like it, if you can also introduce it to your friends. Thank you
github.com/hlc0000/SwiftlyCache/