GardenMobile & interfaces

Flutter foundations: practical application boundaries

The fifth beginner chapter covers localization, data, navigation, versioning, testing, and profiling.

The final beginner chapter moves from a single screen to the boundaries an application has to manage: assets, language, remote data, persistence, navigation, release identity, and evidence that it works. Its localization implementation is historical and has not been rebuilt.

Assets and localization

The source declared asset-based language files:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

flutter:
  uses-material-design: true
  assets:
    - assets/i18n/
# en.json
{
  "title": "Hello!"
}

# vi.json
{
  "title": "Xin chào!"
}

The two language maps belong in separate files. A localization object and delegate loaded them:


import 'dart:convert';

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

class AppLocalizations {
  final Locale locale;

  AppLocalizations(this.locale);

  // Helper method to keep the code in the widgets concise
  // Localizations are accessed using an InheritedWidget "of" syntax
  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations);
  }

  // Static member to have a simple access to the delegate from the MaterialApp
  static const LocalizationsDelegate<AppLocalizations> delegate =
  _AppLocalizationsDelegate();

  Map<String, String> _localizedStrings;

  Future<bool> load() async {
    // Load the language JSON file from the "lang" folder
    String jsonString =
    await rootBundle.loadString('assets/i18n/${locale.languageCode}.json');
    Map<String, dynamic> jsonMap = json.decode(jsonString);

    _localizedStrings = jsonMap.map((key, value) {
      return MapEntry(key, value.toString());
    });

    return true;
  }

  // This method will be called from every widget which needs a localized text
  String translate(String key) {
    return _localizedStrings[key];
  }
}

class _AppLocalizationsDelegate
    extends LocalizationsDelegate<AppLocalizations> {
  // This delegate instance will never change (it doesn't even have fields!)
  // It can provide a constant constructor.
  const _AppLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) {
    // Include all of your supported language codes here
    return ['en', 'vi'].contains(locale.languageCode);
  }

  @override
  Future<AppLocalizations> load(Locale locale) async {
    // AppLocalizations class is where the JSON loading actually runs
    AppLocalizations localizations = new AppLocalizations(locale);
    await localizations.load();
    return localizations;
  }

  @override
  bool shouldReload(_AppLocalizationsDelegate old) => false;
}

The application registered the delegate and read translations through it:

import 'package:flutter_localizations/flutter_localizations.dart';
import 'provider/app_localizations.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      supportedLocales: [
        const Locale('en'),
        const Locale('vi'),
      ],
      localizationsDelegates: [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AppContent(),
    );
  }
}
AppLocalizations.of(context).translate('title')

The old global-locale access and manual selection example were:

import 'dart:ui' as ui;
Locale(ui.window.locale.languageCode, ui.window.locale.countryCode);

import 'dart:ui' as ui;

class MyApp extends StatelessWidget {
  static final localeBloc = BlocDefault<Locale>();

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
        stream: localeBloc.stream,
        builder: (context, snapshot) {
          print(snapshot);
          return MaterialApp(
            locale: (snapshot.hasData
                ? snapshot.data
                : Locale(ui.window.locale.languageCode,
                    ui.window.locale.countryCode)),
            supportedLocales: [
              const Locale('en'),
              const Locale('vi'),
            ],
            localizationsDelegates: [
              AppLocalizations.delegate,
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
            ],
            theme: ThemeData(
              primarySwatch: Colors.blue,
            ),
            home: AppContent(),
          );
        });
  }
}

class AppContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            centerTitle: true,
            title: Text(AppLocalizations.of(context).translate('title'))),
        body: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(
                child: Text('en'),
                onPressed: () {
                  MyApp.localeBloc.push(Locale('en'));
                },
              ),
              RaisedButton(
                child: Text('vi'),
                onPressed: () {
                  MyApp.localeBloc.push(Locale('vi'));
                },
              ),
            ],
          ),
        ));
  }
}

Loading and parsing JSON is asynchronous. The source’s alternative moved the maps into Dart and used bflutter’s localization layer:

# pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  bflutter: ^0.1.8+2
# lib\provider\i18n\en.dart
const en = {
  'title': 'Hello'
};

# lib\provider\i18n\vi.dart
const vi = {
  'title': 'Xin chào'
};
import 'package:flutter/widgets.dart';
import 'package:bflutter/provider/base_localizations.dart';
import 'package:cxi_pos/provider/i18n/en.dart';
import 'package:cxi_pos/provider/i18n/vi.dart';

class AppLocalizations extends BaseLocalizations {
  AppLocalizations(locale) : super(locale);

  // Helper method to keep the code in the widgets concise
  // Localizations are accessed using an InheritedWidget "of" syntax
  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations);
  }

  // Static member to have a simple access to the delegate from the MaterialApp
  static LocalizationsDelegate<AppLocalizations> delegate =
      AppLocalizationsDelegate(
          ['en', 'vi'], (locale) => AppLocalizations(locale));

  @override
  Map<String, String> load(Locale locale) {
    switch (locale.languageCode) {
      case 'vi':
        return vi;
    }
    return en;
  }
}

import 'dart:ui' as ui;

class MyApp extends StatelessWidget {
  static final bloc = MainBloc.instance;

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
        stream: bloc.localeBloc.stream,
        builder: (context, snapshot) {
          return MaterialApp(
            locale: (snapshot.hasData
                ? snapshot.data
                : Locale(ui.window.locale.languageCode,
                    ui.window.locale.countryCode)),
            supportedLocales: [
              const Locale('en'),
              const Locale('vi'),
            ],
            localizationsDelegates: [
              AppLocalizations.delegate,
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
            ],
            theme: ThemeData(
              primarySwatch: Colors.blue,
            ),
            home: AppContent(),
          );
        });
  }
}

class AppContent extends StatelessWidget {

  static final bloc = MainBloc.instance;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            centerTitle: true,
            title: Text(AppLocalizations.of(context).translate('title'))),
        body: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(
                child: Text('en'),
                onPressed: () {
                  bloc.localeBloc.push(Locale('en'));
                },
              ),
              RaisedButton(
                child: Text('vi'),
                onPressed: () {
                  bloc.localeBloc.push(Locale('vi'));
                },
              ),
            ],
          ),
        ));
  }
}

These alternatives have different loading boundaries; neither is a general guarantee against startup stalls. Legacy ui.window and package APIs require version-specific review.

Remote data and persistence

Separate transport failures, response parsing, and presentation state. Cached images and stored application data also have different lifetimes. Flutter’s networking recipe and persistence recipes are useful starting references.

Dialogs, routes, lists, and animation

The original reading list covered Dialog/AlertDialog, route navigation, ListView, and animation. Each adds ownership questions: who closes a route, which state survives navigation, how list items are identified, and who disposes animation resources. The Flutter cookbook groups focused examples.

Version and environment

The source distinguished a semantic version from its build number and recorded these build overrides:

flutter build apk --build-name=1.0.3
flutter build apk --build-number=3
flutter build apk --build-name=1.0.3 --build-number=3

Its environment approach selected a Dart entry point:

flutter run -t lib/Main/main_staging.dart

# Update FLUTTER_TARGET at ios/Flutter/Generated.xcconfig 
FLUTTER_TARGET=lib/Main/main_staging.dart

An environment filename is a configuration convention, not protection for secrets embedded in a client application.

Verify the actual application

The chapter closed with testing, profiling, binding observers, and memory tools. A build result, a functional interaction, and measured frame/memory behavior are separate kinds of evidence. This revision preserves the examples without claiming new application or performance results.

Continue the series

← Back to the garden