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/
pubspec.yaml
to install the libraries, please issue
flutter pub get
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"
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
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"
pubspec.yaml
and to update the app icon, you have to use
dart run flutter_launcher_icons
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(),
);
}
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());
}
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();
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
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,
),
);
}
}
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.
);
}
}
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
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),
];
}
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"
}
assets/locales/en.json
and one for Arabic strings
{
"hello": "hello ar"
}
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
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,
),
);
}
}
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
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),
),
);
}
}
lib/presentation/screens/home/home_screen.dart
English | 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,
),
),
);
}
}
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');
}
}
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();
...
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
});
}
}
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,
),
),
),
);
}
}
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.
);
}
}
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
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)