DEV Community

Cover image for Flutter App initiation, Routing, Dependency Injection, Localization, Splash Screen, App Icon, and more...
Saad Alkentar
Saad Alkentar

Posted on

Flutter App initiation, Routing, Dependency Injection, Localization, Splash Screen, App Icon, and more...

What to expect from this article?

We will start working on the mobile app after building the Backend code in previous articles.

This article only covers Flutter app initiation, selecting the libraries, building project structure (clean architecture of course), and state management technique (bloc and hooks of course)

Before going on, I want to point out this amazing article series by AbdulMuaz Aqeel, his articles detail the project structure, clean architecture concepts, VS code extensions, and many other great details. feel free to use it as a more detailed reference for this article 🤓.

I'll try to cover as many details as possible without boring you, but I still expect you to be familiar with some aspects of Dart and Flutter.

the final version of the source code can be found at https://github.com/saad4software/alive-diary-app

Libraries and their usage

let's start by choosing our library collection, I recommend adding the libraries using flutter pub add ... which guarantees the latest versions, or we can edit the pubspec.yaml file

name: alive_diary_app
description: "A new Flutter project."
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: ^3.5.4

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  retrofit: ^4.1.0 # for making API requests
  dio: ^5.4.3+1 # used by retrofit

  flutter_bloc: ^8.1.5 # bloc state management

  json_annotation: ^4.9.0 # for JSONs
  equatable: ^2.0.5 # for custom object comparison (if needed)
  get_it: ^7.7.0 # for dependency injection
  flutter_hooks: ^0.20.5 # for React-like state management
  auto_route: ^8.1.3 # for routing
  oktoast: ^3.4.0 # a simple toast library
  intl: ^0.19.0
  shared_preferences: ^2.2.3 # for storing local data like JWT token 
  jwt_decoder: ^2.0.1 # to check the token expiration date

  google_fonts: ^6.2.1
  flutter_native_splash: ^2.4.0 # to edit splash screen
  flutter_launcher_icons: ^0.13.1 # to edit app icon
  speech_to_text: ^6.6.2 # convert user speech into text
  flutter_tts: ^3.8.5 # convert text to speech
  avatar_glow: ^3.0.1 # glow effect 
  lottie: ^3.1.2 # to use lottie animations
  easy_localization: ^3.0.7 # to add language support 

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^3.0.0

  build_runner: ^2.4.9
  json_serializable: ^6.8.0

  lint: ^2.3.0
  auto_route_generator: ^8.0.0
  retrofit_generator: ^8.1.0

flutter:
  uses-material-design: true

  assets:
    - assets/images/logo.png
    - assets/images/splash.png
    - assets/locales/
Enter fullscreen mode Exit fullscreen mode

pubspec.yaml

to install the libraries, please issue

flutter pub get
Enter fullscreen mode Exit fullscreen mode

in short

library Description
retrofit API calls
flutter_bloc BLOC State management
get_it Dependency Injection
flutter_hooks React like screen state
auto_route Screen routing
oktoast Simple toast lib
shared_preferences Key-Value local storage
jwt_decoder Decode JWT Expiration date
flutter_native_splash customize native splash screen
flutter_launcher_icons customize app icon
speech_to_text convert user speech to text
flutter_tts text to speech
easy_localization localization lib

Ladybug Android Issues

at the time of writing this article, the Android studio ladybug version had some issues running Flutter project for Android devices, after adding the libraries mentioned before, try running the app, if it didn't run, please check this article for updating android project to work with ladybug android studio

Splash screen with flutter_native_splash

To customize Flutter app splash screen, we need to add splash screen config to pubspec as follows

flutter:
  uses-material-design: true

  assets:
    - assets/images/logo.png
    - assets/images/splash.png
    - assets/locales/

# splash screen icon, make sure it is 1024x1024 and added to the assets
flutter_native_splash:
  android: true
  ios: true
  web: false
  image: assets/images/splash.png
  color: "#FFFFFF"
  android_12:
    image: assets/images/splash.png
    color: "#FFFFFF"
Enter fullscreen mode Exit fullscreen mode

pubspec.yaml

We created an assets folder at the same level as the libs folder. Inside it, we created an images folder and included both splash.png and logo.png.

To update the splash screen icon, we can use

dart run flutter_native_splash:create
Enter fullscreen mode Exit fullscreen mode

App icon with flutter_launcher_icons

Similar to the splash screen, we need to add icon config to the pubspec file like this

flutter:
  uses-material-design: true

  assets:
    - assets/images/logo.png
    - assets/images/splash.png
    - assets/locales/

# app icon, make sure it is added to the assets
flutter_launcher_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/images/logo.png"
  min_sdk_android: 21 # android min sdk min:16, default 21
  web:
    generate: true
    image_path: "assets/images/logo.png"
    background_color: "#hexcode"
    theme_color: "#hexcode"
  windows:
    generate: true
    image_path: "assets/images/logo.png"
    icon_size: 48 # min:48, max:256, default: 48
  macos:
    generate: true
    image_path: "assets/images/logo.png"
Enter fullscreen mode Exit fullscreen mode

pubspec.yaml

and to update the app icon, you have to use

dart run flutter_launcher_icons
Enter fullscreen mode Exit fullscreen mode

Dependency injection with get_it

let's create a dependency injection file for the get_it library

import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';

final locator = GetIt.instance;

Future<void> initializeDependencies() async {
  final dio = Dio();

  locator.registerSingleton<Dio>(dio);

  locator.registerSingleton<SharedPreferences> (
    await SharedPreferences.getInstance(),
  );

}
Enter fullscreen mode Exit fullscreen mode

lib/config/dependencies.dart

We injected the dio and shared preferences object for now, and we will use this to inject the repositories later on. let's not forget to update the main app file

import 'config/dependencies.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDependencies();

  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

lib/main.dart

Screen routing with auto_route

let's create the router config file for auto_route library

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

part 'app_router.gr.dart';


@AutoRouterConfig()
class AppRouter extends _$AppRouter {

  @override
  List<AutoRoute> get routes => [

  ];
}

final appRouter = AppRouter();
Enter fullscreen mode Exit fullscreen mode

lib/config/router/app_router.dart

and to generate the app_router.gr.dart we can issue

dart run build_runner build - delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode

to use it as default routing library, let's update the main app file as follows

import 'config/router/app_router.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: MaterialApp.router(
        routerConfig: appRouter.config(),
        debugShowCheckedModeBanner: false,
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/main.dart

From now on, adding new screens to the app must follow three steps

Step 1: Create the screen widget

We can simply extract the MyHomePage widget from main.dart to a separate screen file for now

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

@RoutePage()
class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key, this.title});

  final String? title;

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title ?? "home"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/home_screen.dart

We have just extracted MyHomePage and renamed it to HomeScreen. make sure to make the screen's parameters optional too.

Step 2: Generate page route

Make sure to add @RoutePage() annotation to the screen widget
in order to generate the router code for this page, then run build_runner

dart run build_runner build - delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode

this should generate this page route in app_router.gr.dart

Step 3: Register the screen route

It's time to register the new screen in the app_router config file

import '../../presentation/screens/home/home_screen.dart';

part 'app_router.gr.dart';

@AutoRouterConfig()
class AppRouter extends _$AppRouter {

  @override
  List<AutoRoute> get routes => [
    AutoRoute(page: HomeRoute.page, initial: true),

  ];
}
Enter fullscreen mode Exit fullscreen mode

lib/config/router/app_router.dart

the auto_route library replaces the word screen with the word route automatically, which is why it is preferable to use the screen prefix for screen widgets. it should work well now, you can run the project

Localization with easy_localization

Let's config the app to support both English and Arabic, in the assets folder, create a locales folder with two JSON files, one for English strings

{
  "hello": "Hello"
}
Enter fullscreen mode Exit fullscreen mode

assets/locales/en.json

and one for Arabic strings

{
  "hello": "hello ar"
}
Enter fullscreen mode Exit fullscreen mode

assets/locales/ar.json

JSON file names should correspond with language names ie. tr.json for Turkish, sp.json for Spanish, and so on. We have to add the locales folder to assets in pubspec

flutter:

  uses-material-design: true

  assets:
    - assets/images/logo.png
    - assets/images/splash.png
    - assets/locales/  # locales folder with the json
Enter fullscreen mode Exit fullscreen mode

pubspec.yaml

and configure it in the main app file

import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; // new

import 'config/dependencies.dart';
import 'config/router/app_router.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDependencies();
  await EasyLocalization.ensureInitialized(); // new

  //new
  runApp(EasyLocalization(
    path: 'assets/locales', // json assets folder
    supportedLocales: const [
      Locale('en'),
      Locale('ar'),
    ],
    fallbackLocale: const Locale('en'),
    assetLoader: CodegenLoader(),
    child: MyApp(),
  ),);

}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: MaterialApp.router(
        // new
        supportedLocales: context.supportedLocales,
        localizationsDelegates: context.localizationDelegates,
        locale: context.locale,

        routerConfig: appRouter.config(),
        debugShowCheckedModeBanner: false,
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/main.dart

to generate the string files, we have to issue

flutter pub run easy_localization:generate -S ./assets/locales -O ./lib/config/locale
Enter fullscreen mode Exit fullscreen mode

Easy localization generate-command should be issued after each update of the JSON, new strings will not be loaded if this command was not issued. it will generate CodegenLoader in lib/config/locale folder. make sure to import it into the main app file.

To test the localization in home_screen, we can add simple text and language change buttons like

import 'package:easy_localization/easy_localization.dart';

class _HomeScreenState extends State<HomeScreen> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title ?? "home"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),

            Text("hello".tr()), // translation key
            ElevatedButton(
              child: const Text("en"),
              onPressed: () async {
                await context.setLocale(const Locale("en"));
              },
            ),

            ElevatedButton(
              child: const Text("ar"),
              onPressed: () async {
                await context.setLocale(const Locale("ar"));
              },
            ),

          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/home/home_screen.dart

English Arabic
App layout in english  App layout in Arabic

Toasting with oktoast

This is a rather simple one 😅, just wrap the main router component with OKToast provider

import 'package:oktoast/oktoast.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: OKToast( // this one
        position: ToastPosition.bottom,
        child: MaterialApp.router(

          supportedLocales: context.supportedLocales,
          localizationsDelegates: context.localizationDelegates,
          locale: context.locale,

          routerConfig: appRouter.config(),
          debugShowCheckedModeBanner: false,
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/main.dart

Now we can use the showToast function anywhere in the app.

BLOC observers and providers

let's create a simple bloc observer to track screen status while debugging

import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class Observer extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    debugPrint('${bloc.runtimeType} $change');
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/utils/bloc_observer.dart

and in the main app file

import 'utils/bloc_observer.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDependencies();
  await EasyLocalization.ensureInitialized();

  Bloc.observer = Observer();
...
Enter fullscreen mode Exit fullscreen mode

We should also add a multi-bloc provider for the app, so let's start by creating home-bloc

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';

@immutable
sealed class HomeEvent {}

@immutable
sealed class HomeState {}

final class HomeInitial extends HomeState {}

class HomeBloc extends Bloc<HomeEvent, HomeState> {
  HomeBloc() : super(HomeInitial()) {
    on<HomeEvent>((event, emit) {
      // TODO: implement event handler
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/home/home_bloc.dart

it is clear that we should move the events and states to separate files 😅, I've written it like this since it is an empty bloc for now

and in the main app file, we need to add the multi-bloc provider

import 'presentation/screens/home/home_bloc.dart';

...

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: MultiBlocProvider(
        providers: [
          BlocProvider(create: (context)=>HomeBloc()),
        ],
        child: OKToast(
          position: ToastPosition.bottom,
          child: MaterialApp.router(

            supportedLocales: context.supportedLocales,
            localizationsDelegates: context.localizationDelegates,
            locale: context.locale,

            routerConfig: appRouter.config(),
            debugShowCheckedModeBanner: false,
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/main.dart

This provider allows the injected blocs to be fetched anywhere in the widget tree.

Flutter hooks showcase

Well, this is not related to our app, so feel free to ignore it. Hooks is an alternative to stateful widgets. As a showcase for it, we will convert the stateful home screen widget to a hook widget 🤓,

import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:oktoast/oktoast.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

@RoutePage()
class HomeScreen extends HookWidget {
  const HomeScreen({super.key, this.title});

  final String? title;

  @override
  Widget build(BuildContext context) {

    final counter = useState(0);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(title ?? "home"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counter.value}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),

            Text("hello".tr()),
            ElevatedButton(
              child: const Text("en"),
              onPressed: () async {
                await context.setLocale(const Locale("en"));
                showToast("nice one");
              },
            ),

            ElevatedButton(
              child: const Text("ar"),
              onPressed: () async {
                await context.setLocale(const Locale("ar"));
              },
            ),

          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: ()=>counter.value++,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

lib/presentation/home_screen.dart

as you can see, it is simpler and smoother code. changing the counter value will only update the text string on the screen

Project structure, clean architecture (minimized)

Clean architecture is based on three layers (refer to this for more details)

  • Domain Layer: it is the most important layer, every change starts here, it contains the repository interfaces and data models.
  • Data Layer: it is where we implement the domain interfaces and data sources. it can have its own models, but we will use the same models from the domain layer to simplify the implementation (that is what we mean by minimized)
  • Presentation layer: contains the UI screens and their state management (bloc)

for now, let's simply follow this folder structure

lib
____config
________locale # already used for easy localization
________router # already used for auto-route
________theme

____data
________sources # remote and local data sources
________repositories # domain repository implementation

____domain
________models # all models in the app
________repositories # repositories interfaces

____presentation
________blocs # generic blocs
________screens # screens and their blocs
________widgets # widgets to use in multiple screens
Enter fullscreen mode Exit fullscreen mode

That is it!
I know it has been a long article, but I believe those libraries to be a MUST in any Flutter app, and we haven't implemented anything yet, just added the libraries and made sure they work.

we will start working on the models and API next, so

Stay tuned 😎

Top comments (0)