Flutter scalable app skeleton. Application bones.

Orexjeka
4 min readJul 6, 2024

--

DI, ENV, Folder structure, Networking, Assets & Simple design system.

Flutter 3.22.1, Dart 3.4.1. Mac OS. VS Code.

Be sure you seen a Setup

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.

--

--