What the Flutter

This is our journey using Flutter

Who are we?

What we set out to do

Convert Angular app to Flutter

100% Custom Design

Support MQTT (real-time)

Support GraphQL

~200 Screens

~4 Months

What is this Flutter thing?

"Flutter is an open-source UI software development kit created by Google. It is used to develop applications for Android, iOS, Windows, Mac, Linux, Google Fuchsia. and the web. The first version of Flutter was known as codename "Sky" and ran on the Android operating system."

(web, fuchsia, etc.)

(dart)

Pros

  • Single codebase
  • Practically native
  • Active community
  • Extremely good tooling
    • Hot Reload !
    • dartfmt (Autoformatting)
    • Flutter inspector
    • Flutter Performance
  • Dart

Cons

  • Relatively new
    • Lack of examples
    • Lack of 3rd party libraries
    • Camera support
    • etc.
  • Not everything is possible* (yet)
    • Native widgets
    • Compile to x86
    • Apple Watch / Android Watch
    • Running code in background
  • Few enterprise examples*
  • Dart...?

Experience 

It's not web development

Nor is it native development

Widgets all the way

import 'package:flutter/material.dart';

class FhFullScreenLoading extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Container(
          child: const Center(
            child: Text('Loading...'),
          ),
        ),
      ),
    );
  }
}

Custom widgets

import 'package:fh_ui/fh_ui.dart';
import 'package:flutter/material.dart';

class FhTwoToneText extends StatelessWidget {
  const FhTwoToneText(this.first, this.second);

  final String first;
  final String second;

  @override
  Widget build(BuildContext context) {
    return RichText(
      text: TextSpan(
        children: <TextSpan>[
          TextSpan(
            text: first,
            style: TextStyles.openSans14regular.copyWith(FhColors.darkBlue),
          ),
          TextSpan(
            text: second,
            style: TextStyles.openSans14light.copyWith(FhColors.darkBlue),
          ),
        ],
      ),
    );
  }
}

custom slider

Dart DevTools: Flutter Inspector

Dart DevTools: Memory Profiling

Dart DevTools: Logger

Dart is great...

"Dart is an object-oriented, class defined, garbage-collected language using a C-style syntax that transcompiles optionally into JavaScript. It supports interfaces, mixins, abstract classes, reified generics, static typing, and a sound type system."

... until it's not

and silently just fails

Lack of enterprise examples

State Management & Architecture

BLoC

(flutter_bloc)

MQTT

HTTP

(DB)

xDataProvider

yDataProvider

xRepository

BLoC

Widgets

yRepository

zRepository

BehaviorSubject<Profile> profile$ = BehaviorSubject<Profile>.seeded(null);

Future<Profile> fetchProfile() async {
  final Uri uri = fhHttpClient.getUri(heimdallEndpoint, profilePath);

  final http.Response response = await fhHttpClient.get(uri);

  if (response.statusCode == 200) {
    final dynamic body = jsonDecode(response.body);
    final Profile profile = Profile.fromJson(body);
    profile$.add(profile);
    return profile;
  } else {
    _logger.warning('Could not fetch profile.');
    return null;
  }
}

profile_data_provider

Stream<Profile> getProfile() async* {
  await for (final Profile profile in profile$) {
    if (profile != null) {
      yield profile;
    }
  }
}

profile_data_provider

Stream<Profile> getProfile() async* {
  yield* _profileDataProvider.getProfile();
}

profile_repository

This is where you can bring in other data providers

ProfileHomeBloc() {
  _subscription = _profileDataProvider.getProfile().listen(
    (Profile profile) {
      add(SyncProfileEvent(profile));
    },
  );
}

profile_bloc

@override
Stream<ProfileHomeState> mapEventToState(
  ProfileHomeEvent event,
) async* {
  if (event is InitProfileHomeEvent) {
    yield* _mapInitProfileHomeEventToState();
  } else if (event is SyncProfileEvent) {
    yield* _mapSyncProfileEventToState(event.profile);
  }
}

profile_bloc

Stream<ProfileHomeState> _mapSyncProfileEventToState(
  Profile profile,
) async* {
  yield ProfileHomeLoadedState(profile);
}

profile_bloc

body: BlocProvider<ProfileHomeBloc>(
  builder: (BuildContext context) => ProfileHomeBloc(),
  child: BlocBuilder<ProfileHomeBloc, ProfileHomeState>(
    builder: (BuildContext context, ProfileHomeState state) {
      if (state is ProfileHomeLoadingState) {
        return FhFullScreenLoading();
      } else if (state is ProfileHomeLoadedState) {
        return ProfileHomeBody(state.profile);
      } else if (state is ProfileHomeErrorState) {
        return FhFullScreenError();
      } else {
        return const Center(
          child: Text('ProfileHome'),
        );
      }
    },
  ),
),

profile_screen

Applying DevOps in Flutter Mobile Development 

CI / CD

What is difficult of CI in Mobile development

 

1- Android Emulator required for instrumentation tests

2- iOS simulator required for iOS UI tests

3- iOS apps cannot be build on linux / windows

4- Shared code signing for all teams/members

Tools?

 

Travis CI

Circle CI

Jenkins

Github Actions 

GitLab CI

Code Magic 

Never Code

Fastlane

  1. Capture screenshots automatically
  2. Distribute beta builds
  3. publish your app with the push of a button
  4. automate code signing for iOS
  5. Handle your build by code (code as infrastructure)  
  6. Open source / Written in Ruby / Gem

Commandline

platform :android do

# your lanes for Android here 

end


platform :ios do

# your lanes for iOS here 

end
platform :android do

  desc "Build and release to beta"
  lane :beta_internal_release do
    prepare(release: false)
    setup_new_changelog
    release_to_store(track: 'internal')
  end
  
end
platform :ios do

  desc "Push a new release to TestFlight"
  lane :build_and_deploy_testflight do |options|
    setup_travis
    prepare(release: false)
    setup_new_changelog
    upload_to_testflight(
        skip_waiting_for_build_processing: true,
        ipa: "Runner.ipa",
        username: username,
        changelog: changelog,
    )
    slack(
        message: "#{version_number}+#{build_number}: 
        iOS beta released to TestFlight \n 
        Changelog: \n #{emojified_changelog}",
        slack_url: slack_url,
    )
    # git_actions(release: false)
  end

end
platform :ios do

  desc "Prepare and archive app"
  lane :prepare do |options|
    puts "Building and deploying version #{version_number}, current build #{latest_testflight_build_number}"
    handle_versioning
    flutter_build_no_sign
    generate_icons
    beta_badge(hide: options[:release])
    code_signing
    build_ios_app(
        workspace: workspace_path,
        configuration: "Release",
        scheme: "Runner",
        silent: true,
        clean: true,
        export_team_id: export_team_id,
        export_method: "app-store",
        # Verify that the right signing identity is used for publishing.
        codesigning_identity: codesigning_identity,
        xcargs: "DEVELOPMENT_TEAM='#{export_team_id}'"
    )
  end
end

Share one code signing identity across your development team to simplify your codesigning setup and prevent code signing issues.

 

codesigning.guide

 

match

🔄 Automatically sync your iOS keys and profiles across all your team members using git
📦 Handle all the heavy lifting of creating and storing your certificates and profiles
💻 Setup codesigning on a new machine in under a minute
🎯 Designed to work with apps with multiple targets and bundle identifiers
🔒 You have full control over your files and Git repo, no third party service involved
Provisioning profile will always match the correct certificate
💥 Easily reset your existing profiles and certificates if your current account has expired or invalid profiles
♻️ Automatically renew your provisioning profiles to include all your devices using the --force option
👥 Support for multiple Apple accounts and multiple teams
Tightly integrated with fastlane to work seamlessly with gym and other build tools
#Matchfile

git_url("git@github.com:YOURACCOUNT/YOUR_CERT_REPO.git")
storage_mode("git")
type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
app_identifier(["no.futurehome.futurehomeApp"])
username("majid@futurehome.no") # Your Apple Developer Portal username

Google Cloud Storage

Github

platform :ios do
  desc "Sign code using Match and fastlane"
  lane :code_signing do
    # Stop fastlane from echoing back ENV var.
    # Doesn't matter too much since Travis doesn't echo back encrypted variables
    # anyway :D
    suppress_output {
      # Retrieves all the necessary certs and provisioning profiles.
      # match   # alias for "sync_code_signing"
      sync_code_signing(readonly: true)
    }
    puts 'Certificates and profiles installed'
    # Modify the Xcode project to use the new team and profile.
    # It will put the git state to dirty but Travis will be wiped after
    # then run session.
    disable_automatic_code_signing
    update_project_provisioning(
        build_configuration: 'Release',
        profile: ENV["sigh_#{app_identifier}_appstore_profile-path"]
    )
  end
end
platform :ios do
  desc "Handle version and build number"
  lane :handle_versioning do
    build_number = latest_testflight_build_number + 1
    puts "new build number now is #{build_number} and build version is #{version_number}"
  end
end




platform :android do
  desc "Handle version and build number"
  lane :handle_versioning do
    build_number = current_build + 1
    puts "new build number now is #{build_number} and build version is #{raw_version}"
  end
end

Build Version

platform :ios do
  desc "Build app with no signing for ios and increase build number or version number"
  lane :flutter_build_no_sign do
    Dir.chdir "../#{project_ios_path}" do
      sh("flutter", "doctor")
      sh("flutter", "packages", "get")
      sh("flutter", "clean")
      sh("flutter", "build", "ios", "--build-number=#{build_number}", "--build-name=#{version_number} ", "--release", "--no-codesign")
    end
  end
end

Run Test / other commands

platform :android do
  desc "Create change log and make it ready for both slack and google play"
  lane :setup_new_changelog do
    changelog = read_changelog(
        changelog_path: "../CHANGELOG.md",
        section_identifier: "[Build: #{version_number}]", # Specify what section to read
        excluded_markdown_elements: ['-', '###'] # Specify which markdown elements should be excluded
    )
    emojified_changelog = emojify_changelog

    # write for Google store at the moment we have only no-NO
    path = "./metadata/android/no-NO/changelogs"
    dir = File.dirname(path)
    unless File.directory?(dir)
      FileUtils.mkdir_p(dir)
    end
    path << "/#{build_number}.txt"
    File.new(path, 'w')
    File.write(path, changelog)

    puts "Changelog for version #{version_number}+#{build_number}: #{changelog}"
  end
end

Change Log / Metadata

platform :android do
  
  desc "update git"
  lane :git_actions do
    # Ensure that your git status is not dirty
    ensure_git_status_clean
    #add git tag
    add_git_tag(build_number: version_number)
    # Commit the version bump
    commit_version_bump
    # Push the new commit and tag back to your git remote
    push_to_git_remote
  end
  
  # and many more 
  # capture screenshots for android
  # upload to Google Play
  # Run tests
  # generate icons
  # add beta badge 
  # lots of other plugins 
end

more Lanes

Summary

Thanks

Q/A?

What the Flutter

By Majid Hajian

What the Flutter

  • 1,203