GardenMobile & interfaces

Flutter foundations: an earlier App Center and launch setup

A historical third chapter covering telemetry integration, CI, signing, launcher icons, and native splash resources.

The third chapter prepared an application for telemetry, automated builds, and launch presentation. Its tools and plugins are now historical; the useful structure is the separation between runtime integration, build delivery, signing, and visual assets.

Historical service workflow. App Center Build and Distribution are part of the retired service workflow. Microsoft documents separate Analytics/Diagnostics extensions in its retirement notice. The old CI steps below are not a current hosted-build setup.

The earlier telemetry integration

The source created separate Android and iOS application entries and used these Git-based plugins:

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
  appcenter:
    git:
      url: git://github.com/nhancv/flutter_plugin_appcenter.git
      path: src/appcenter
  appcenter_crashes:
    git:
      url: git://github.com/nhancv/flutter_plugin_appcenter.git
      path: src/appcenter_crashes
  appcenter_analytics:
    git:
      url: git://github.com/nhancv/flutter_plugin_appcenter.git
      path: src/appcenter_analytics

Its application startup and demonstration screen were:

Read the historical startup example
import 'package:flutter/material.dart';
import 'package:appcenter/appcenter.dart';
import 'package:appcenter_analytics/appcenter_analytics.dart';
import 'package:appcenter_crashes/appcenter_crashes.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/foundation.dart' show TargetPlatform;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // @nhancv 10/23/2019: Config app center
  final ios = defaultTargetPlatform == TargetPlatform.iOS;
  String appSecret = ios
      ? "EXAMPLE_IOS_APP_ID"
      : "EXAMPLE_ANDROID_APP_ID";
  await AppCenter.start(
      appSecret, [AppCenterAnalytics.id, AppCenterCrashes.id]);

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CrashHome(),
    );
  }
}

class CrashHome extends StatefulWidget {
  @override
  _CrashHomeState createState() => _CrashHomeState();
}

class _CrashHomeState extends State<CrashHome> {
  String _installId = 'Unknown';
  bool _areAnalyticsEnabled = false, _areCrashesEnabled = false;

  @override
  initState() {
    super.initState();
    initPlatformState();
  }

  initPlatformState() async {
    if (!mounted) return;

    var installId = await AppCenter.installId;
    var areAnalyticsEnabled = await AppCenterAnalytics.isEnabled;
    var areCrashesEnabled = await AppCenterCrashes.isEnabled;

    setState(() {
      _installId = installId;
      _areAnalyticsEnabled = areAnalyticsEnabled;
      _areCrashesEnabled = areCrashesEnabled;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Appcenter plugin example app'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Text('Install identifier:\n $_installId'),
          Text('Analytics: $_areAnalyticsEnabled'),
          Text('Crashes: $_areCrashesEnabled'),
          RaisedButton(
            child: Text('Generate test crash'),
            onPressed: AppCenterCrashes.generateTestCrash,
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Text('Send events'),
              IconButton(
                icon: Icon(Icons.map),
                tooltip: 'map',
                onPressed: () {
                  AppCenterAnalytics.trackEvent("map");
                },
              ),
              IconButton(
                icon: Icon(Icons.casino),
                tooltip: 'casino',
                onPressed: () {
                  AppCenterAnalytics.trackEvent("casino", {"dollars": "10"});
                },
              ),
            ],
          ),
        ],
      ),
    );
  }
}

The project identifiers have been replaced with example strings. The demo can generate a test crash and send events; no such actions were performed for this revision.

The old iOS workaround and Android permission were:

target 'Runner' do
  # use_frameworks!
    <uses-permission android:name="android.permission.INTERNET"/>

These describe the earlier dependencies, not universally valid fixes for later CocoaPods or SDK versions.

Keep CI and signing separate

The original CI used platform-specific appcenter-post-clone.sh scripts, repository detection, and dashboard build/distribution controls. It also cloned a debug keystore for release signing. That signing recipe is omitted: a debug credential is not a deliberate production signing identity. No keystore, signing password, build account, or distribution email was imported.

Generate the launcher assets

dev_dependencies:
  flutter_launcher_icons: ^0.7.3

flutter_icons:
  image_path: "assets/icons/app-icon.png"
  android: true
  ios: true
  image_path_android: "assets/icons/app-icon.png"
  image_path_ios: "assets/icons/app-icon.png"
  adaptive_icon_background: "assets/icons/app-icon.png"
  adaptive_icon_foreground: "assets/icons/app-icon.png"
flutter pub get
flutter pub run flutter_launcher_icons:main

The source used flutter_launcher_icons ^0.7.3; this records its declared dependency rather than a fresh compatibility check.

The earlier splash treatment

Android used a drawable and launch theme:

<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/white" />

    <!-- You can insert your own image assets here -->
    <item>
        <bitmap
            android:gravity="center"
            android:src="@drawable/ic_launcher_background" />
    </item>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
        <!-- Show a splash screen on the activity. Automatically removed when
             Flutter draws its first frame -->
        <item name="android:windowBackground">@drawable/launch_background</item>
        <item name= "android:windowFullscreen">true</item>
    </style>
</resources>

Its embedding-specific activity variants were:

import android.os.Build
import android.os.Bundle
import android.view.ViewTreeObserver
import android.view.WindowManager

import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant

class MainActivity : FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //make transparent status bar
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.statusBarColor = 0x00000000
        }
        GeneratedPluginRegistrant.registerWith(this)
        //Remove full screen flag after load
        val vto: ViewTreeObserver = flutterView.viewTreeObserver
        vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                flutterView.viewTreeObserver.removeOnGlobalLayoutListener(this)
                window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
            }
        })
    }
}
import android.os.Build
import android.os.Bundle
import android.view.ViewTreeObserver
import android.view.WindowManager

import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant

public class MainActivity extends FlutterActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //make transparent status bar
        getWindow().setStatusBarColor(0x00000000);
        GeneratedPluginRegistrant.registerWith(this);
        //Remove full screen flag after load
        ViewTreeObserver vto = getFlutterView().getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                getFlutterView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            }
        });
    }
}

The original iOS asset note was:

Open Runner.xcworkspace -> Runner -> Runner -> Assets.scassets -> LaunchImage -> Upload 3 type res

The source’s Java-labeled activity excerpt is preserved as supplied rather than silently represented as a verified Java build. No current application was built or launched for this revision.

Continue the series

← Back to the garden