- Setup. Dependencies, snippets and scripts.
- Application bones. DI, ENV, Folder structure, Networking, Assets & Simple design system.
- State management. Redux.
- Navigation. with AutoRoute.
- Features. Implementing simple app functionality.
- Unit tests. with Mockito.
Flutter community is amazingly powerful. There is a plenty wonderful packages at pub.dev. But how to chose the best tools and libraries, the most suitable for you project?
DI is a way how you are create, store and connect your objects together. We are going to use GetIt + Injectable where injectable is a generator for GetIt DI container
// lib/main.dart
Future<void> main() async {
runZonedGuarded(
() async {
await configureDependencies();
WidgetsFlutterBinding.ensureInitialized();
runApp(
RecordApp(
store: createStore(),
),
);
},
(error, stackTrace) async {
// track error
},
);
}
// lib/DI/get_it.dart
import 'package:get_it/get_it.dart';
final GetIt getIt = GetIt.instance;
// lib/utils/environment/getEnv.dart
EnvType getEnv() {
const env = String.fromEnvironment('ENV', defaultValue: 'debug');
switch (env) {
case debugRaw:
return EnvType.debug;
case profileRaw:
return EnvType.profile;
case releaseRaw:
return EnvType.release;
default:
return EnvType.debug;
}
}
// lib/repositories/env_vars/env_vars_impl.dart
@Singleton(as: EnvVars)
class EnvVarsImpl extends EnvVars {
@FactoryMethod(preResolve: true)
static Future<EnvVars> create() async {
await dotenv.load(fileName: _getEnvFileName());
return EnvVarsImpl();
}
@override
String get baseApiUrl => dotenv.get('base_api_url');
}
put into assets/env/.env.x files a variable of your api url
base_api_url='http://localhost:8080'
and of course, we need a get_it initialization that runs in main.dart
@InjectableInit(
initializerName: r'$initGetIt',
preferRelativeImports: true,
asExtension: true,
)
Future<void> configureDependencies() async {
await getIt.$initGetIt(environment: getEnv().rawValue);
}
Going to the network layer, my choice was fell on Dio + Reftrofit, where is, again, Retrofit is an generator for DIO providing really nice shell within metadata annotations
// lib/DI/third_party/dio_inject.dart
@module
abstract class DioModuleTag {
@Named("BaseUrl")
String get baseUrl => getIt<EnvVars>().baseUrl;
@singleton
Dio dio(@Named('BaseUrl') String url) => Dio(BaseOptions(baseUrl: url));
}
Declaring api
// lib/services/api/record_app_rest_api.dart
import 'package:dio/dio.dart';
// ignore: depend_on_referenced_packages
import 'package:retrofit/retrofit.dart';
part 'record_app_rest_api.g.dart';
@RestApi()
abstract class RecordAppRestApi {
factory RecordAppRestApi(Dio dio) = _RecordAppRestApi;
@POST('/user-query')
@MultiPart()
Future<void> userQuery(
@Part() String name,
@Part() String phone,
@Part() List<MultipartFile> files,
);
}
and register it
// lib/DI/third_party/retrofit_rest_api_inject.dart
@module
abstract class RecordAppRestApiModuleTag {
RecordAppRestApi build(Dio dio) => RecordAppRestApi(dio);
}
Run a generator from the the Setup article.
Access RecordAppRestApi
object using getIt<RecordAppRestApi>()
already, congratulations!
Design system
And now we are just about Design system. Basically DS is a neat wrapper that help you care less about concrete values and operate with a predefines sets of abstractions. Depends on project maturity eventually all app can be described in the respective DS framework. But lets see the basics
Atoms:
// lib/design_system/atoms/size_type.dart
enum SizeType {
xxl,
xl,
l,
m,
s,
xs,
}
// lib/design_system/atoms/spacings.dart
class Spacings {
Spacings._();
// Mobile Spacings
static const double mobileXXl = 22.0;
static const double mobileXl = 18.0;
static const double mobileL = 14.0;
static const double mobileM = 10.0;
static const double mobileS = 8.0;
static const double mobileXs = 6.0;
// Tablet Spacings
static const double tabletXXl = 26.0;
static const double tabletXl = 20.0;
static const double tabletL = 16.0;
static const double tabletM = 12.0;
static const double tabletS = 10.0;
static const double tabletXs = 8.0;
// Desktop Spacings
static const double desktopXXl = 28.0;
static const double desktopXl = 22.0;
static const double desktopL = 18.0;
static const double desktopM = 14.0;
static const double desktopS = 12.0;
static const double desktopXs = 10.0;
}
// lib/design_system/atoms/text_type.dart
enum TextType {
heading1,
heading2,
heading3,
body1,
body2,
body3,
}
// lib/design_system/atoms/text_styles.dart
class TextStyles {
TextStyles._();
// Mobile
static const TextStyle mobileHeading1 = TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold,
);
static const TextStyle mobileHeading2 = TextStyle(
fontSize: 22.0,
fontWeight: FontWeight.bold,
);
static const TextStyle mobileBody1 = TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
);
....
// Tablet
static const TextStyle tabletHeading1 = TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.bold,
);
.....
}
Molecules:
// lib/design_system/molecules/spacing_fun.dart
double spacingFun(BuildContext context, SizeType type) {
switch (type) {
case SizeType.xxl:
return onDevice(
context,
desktop: (context) => Spacings.desktopXXl,
tablet: (context) => Spacings.tabletXXl,
mobile: (context) => Spacings.mobileXXl,
);
case SizeType.xl:
return onDevice(
context,
desktop: (context) => Spacings.desktopXl,
tablet: (context) => Spacings.tabletXl,
...
}
// lib/design_system/molecules/text_style_fun.dart
TextStyle textStyleFun(
BuildContext context,
TextType type,
Color color,
) {
final TextStyle style;
switch (type) {
case TextType.heading1:
style = onDevice(
context,
desktop: (context) => TextStyles.desktopHeading1,
tablet: (context) => TextStyles.tabletHeading1,
mobile: (context) => TextStyles.mobileHeading1,
);
case TextType.heading2:
style = onDevice(
context,
desktop: (context) => TextStyles.mobileHeading2,
);
....
}
return style.copyWith(color: color);
}
// lib/design_system/molecules/theme_data_func.dart
import 'package:flutter/material.dart';
ThemeData themeDataFun() {
return ThemeData(
useMaterial3: true,
fontFamily: 'Roboto',
colorScheme: const ColorScheme(
brightness: Brightness.light,
primary: Color.fromARGB(255, 0, 90, 193), // extract to atoms ideally
onPrimary: Color.fromARGB(255, 255, 255, 255),
onPrimaryContainer: Color.fromARGB(255, 213, 213, 213),
secondary: Color.fromARGB(255, 249, 246, 246),
onSecondary: Color.fromARGB(255, 0, 0, 0),
error: Color.fromARGB(255, 193, 0, 16),
onError: Color.fromARGB(255, 255, 255, 255),
surface: Color.fromARGB(255, 239, 240, 251),
onSurface: Color.fromARGB(255, 0, 0, 0),
shadow: Color.fromARGB(187, 0, 0, 0),
),
);
}
And for the case of a customizing, lets say i want an additional color:
// lib/design_system/color_sheme_ds_extensions.dart
extension ColorShemeDsExtensions on ColorScheme {
Color get shadowLight => shadow.withAlpha(125);
}
One more tiny extension:
// lib/design_system/build_context_ds_extensions.dart
extension BuildContextDsExtensions on BuildContext {
ColorScheme get colorScheme => Theme.of(this).colorScheme;
TextStyle textStyle(TextType style, Color color) {
return textStyleFun(this, style, color);
}
double spacing(SizeType size) {
return spacingFun(this, size);
}
}
Usage
Great! See how is it 👨🏽🎨
// component example
body: Container(
color: context.colorScheme.surface,
child: Padding(
padding: EdgeInsets.only(
left: context.spacing(SizeType.m),
right: context.spacing(SizeType.),
),
child: Center(
child: Text(
'Text',
style: context.textStyle(
TextType.body2,
context.colorScheme.onSurface,
),
),
),
),
Application widget can be updated as well
// lib/app.dart
class RecordApp extends StatelessWidget {
final Store<AppState> store;
const RecordApp({
required this.store,
super.key,
});
@override
Widget build(BuildContext context) {
return StoreProvider(
store: store,
// Wrapping up to make layout responsiveness
child: Layout(
child: MaterialApp.router(
theme: themeDataFun(),
// see Routing article
routerConfig: getIt<AppRouter>().config(
navigatorObservers: () => [
AutoRouteObserver(),
],
),
),
),
);
}
}
Summary:
Breaking down your application into smaller, manageable modules makes it easier to maintain and scale
Generic problems have a generic solutions empowered by Flutter community.
🔻 flutter-web is less supported across packages
Autogeneration speed up your development and align your code with a best practices
Thank you, see you next posts.