It is the folder architecture created by my approach. It maybe useful with your contribution to improve architecture. Let's explain about each part more deeply.
Keep in mind that, following numbered parts are folders or dart files.
This is layer is used to handle logical operations. It maybe other state management too. It consists of 2 parts:
- Blocs
For each bloc, I create folder and it contains cubit or bloc, events and states.
- login
- login_bloc (login_cubit)
- login_event
- login_state
- Bloc Observer (optional) BlocObserver is simple delegate to handle all operations of Bloc in one place. You can learn about Bloc using this link.
Configrations are added to this folder. It contains the followings:
- config.dart
We can store main configrations in this file:
abstract class Configs {
static const String SENTRY_DSN ='...';
static const bool enableLogging = kDebugMode;
static const String baseUrl = '...';
}
- init.dart
This file contains simple method that initializes all services and needed things before app started:
Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized();
final locator = Locator.instance;
final dio = Dio();
if(Configs.enableLogging) {
LoggingService(Configs.enableLogging);
Bloc.observer = AppBlocObserver();
dio.interceptors.add(LogInterceptorService());
}
locator.register<AuthService>(AuthService(dio));
await locator.registerAsync<PreferencesService>(() async {
final instance = PreferencesService.instance;
await instance.init();
return instance;
});
}
As a name implies we store contants of app in this folder. It maybe followings and more:
- app_text_styles.dart
Text styles of app are stored in this folder:
abstract class AppTextStyles {
static const interW500 = TextStyle(
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
);
}
- app_themes.dart
This file can contain light and dart theme details:
abstract class AppThemes {
static final theme = ThemeData(
fontFamily: Assets.fonts.fontFamily,
primaryColor: AppColors.primary,
accentColor: AppColors.primary,
scaffoldBackgroundColor: Colors.white,
textTheme: TextTheme(
bodyText1: AppTextStyles.interW500.copyWith(color: AppColors.altoBlack),
),
);
}
- app_colors.dart
It just contains colors of app:
abstract class AppColors {
static const navyGrey = Color.fromRGBO(129, 140, 153, 1);
static const greenGradient = Color(0xffF0FAEA);
}
- routes.dart
This file stores static paths of app for routing:
abstract class Routes {
static const String signIn = '/signIn';
static const String goods = '/goods';
}
- assets.dart
To access assets easily, we can store them in one place like this:
abstract class Assets {
static final images = Images();
static final sounds = Sounds();
static final fonts = Fonts();
}
class _Images {
static final _basePath = 'assets/images';
final icon = '$_basePath/icon.png';
}
class _Sounds {
...
}
class _Fonts {
final fontFamily = 'Inter';
}
This is the one of the most useful part of app. It contains all data and its processes.
- contractors
It is like a protocol or interface (abstract class in Dart) to follow SOLID principles:
base_auth_repository.dart
abstract class BaseAuthRepository {
Future<Response> login();
Future<void> logOut();
}
It will be used by
repositories
as interfaces to follow rules. If we need adad something torepositories
, first we will add it to contractors. I mainly advice to divide contractors correctly - auth, orders, users and etc.
- models
As name implies, it is just simple entity container. You can divide it to 2 folders like - response
and request
models.
- models
- response
order_details.dart
- request
order_details_request_body.dart
- repositories It is just implementations of contractors. then, it will be injected to Blocs using contractors reference (look at DI (Dependency Inversion) principle of SOLID).
Keep in mind that it can access to all services (next part).
class AuthRepository implements BaseAuthRepository {
final _authService = locator.get<AuthService>();
Future<Response> login() {
...
return _authService.login();
}
Future<void> logOut() {
...
return _authService.logOut();
}
}
- services
Services maybe all things like - data services that fetching data from api, database, payment service, location service or someting else. I usually use retrofit
to write api methods fastly. And we should register services in init.dart
by locator (getIt or someting else) to use inside app.
@RestApi(baseUrl: Configs.baseUrl)
abstract class AuthService {
factory AuthService(Dio dio, {String baseUrl}) = _AuthService;
@POST('/login')
Future<Response> login();
}
It is the new localization approach of Flutter. This folder just stores *.arb
files for different languages. You can read about it from here.
Presentation contains all UI codes and integration with blocs. It is divided into different parts.
- dialogs
You can apart your dialogs code like pages in other place to handle easly:
For each dialog, pages me creating a folder like
info
and inside info folderinfo_dialog.dart
. If this dialog has some widgets you can apart them to other folder insideinfo
folder towidgets
folder.
class InfoDialog extends StatelessWidget {
const InfoDialog({
Key? key,
required this.message,
}) : super(key: key);
final String message;
@override
Widget build(BuildContext context) {
...
}
}
- pages
Pages is the same with dialogs approach.
- global
If a widget used in many places, you can insert them inside global
folder to use several places.
- router
I usually use
onGenerateRoute
method ofMaterialApp
(orWidgetsApp
,CupertinoApp
) to makenamed routing
. You can also useNavigation 2.0
approach.
class AppRouter {
AppRouter._();
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case Routes.signIn:
return MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => SignInCubit(
context.read<AuthRepository>(),
),
child: SignInPage(),
),
);
default:
throw UnimplementedError('No defined route: ${settings.name}');
}
- app.dart
Generally, I prefer to apart main.dart
from widgets. That is why. app.dart
file contains starting configrations of app. Then we will call it from main.dart
.
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
return MaterialApp(
debugShowCheckedModeBanner: kDebugMode,
theme: AppThemes.theme,
locale: locale,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
home: AuthPage(),
onGenerateRoute: AppRouter.onGenerateRoute,
);
You can inject your main repositories or blocs in this place, for example
AuthCubit
,AuthRepository
,LanguageCubit
,PreferencesRepository
and etc.
Utils can be anything - mixin
, extension
or other helper methods, classes. You can divide them by folder like - mixins
, extensions
, others
.
It is the simple DI (Dependency Injection) Containe
I have written for myself. You can also use getIt
or other packages.
typedef AsyncRegister<T> = Future<T> Function();
late final locator = Locator.instance;
class Locator {
Locator._();
static _Locator? _instance = _Locator();
static _Locator get instance => _instance!;
T get<T>() => _instance!.get<T>();
void register<T>(T instance) => _instance!.register<T>(instance);
Future<void> registerAsync<T>(AsyncRegister<T> asyncBuilder) =>
_instance!.registerAsync<T>(asyncBuilder);
void close() {
_instance!.close();
_instance = null;
}
}
class _Locator implements Locator {
final _services = <Type, dynamic>{};
@override
T get<T>() {
if (_services.containsKey(T)) return _services[T]!;
throw LocatorNotFoundException();
}
@override
void register<T>(T instance) {
_services.putIfAbsent(T, () => instance);
}
@override
Future<void> registerAsync<T>(AsyncRegister<T> asyncBuilder) async {
final instance = await asyncBuilder.call();
_services.putIfAbsent(T, () => instance);
}
@override
void close() {
_services.clear();
}
}
class LocatorNotFoundException implements Exception {}
It will be just simple void method to start app:
void main() async {
// calling `init.dart` file to initialize services
await init();
runApp(App())
}