? buildWhen,
15 | }) {
16 | return BlocBuilder(
17 | bloc: presenter,
18 | buildWhen: buildWhen,
19 | builder: builder,
20 | );
21 | }
22 |
23 | @override
24 | void dispose() {
25 | super.dispose();
26 | presenter.close();
27 | }
28 | }
29 |
30 | mixin HasPresenter
on StatefulWidget {
31 | P get presenter;
32 | }
33 |
34 | mixin CubitToCubitCommunicationMixin on Cubit {
35 | final _subscriptions = >[];
36 |
37 | void listenTo(Cubit cubit, {required void Function(C) onChange}) =>
38 | addSubscription(cubit.stream.listen(onChange));
39 |
40 | void addSubscription(StreamSubscription subscription) {
41 | _subscriptions.add(subscription);
42 | }
43 |
44 | @override
45 | Future close() async {
46 | await Future.wait(_subscriptions.map((it) => it.cancel()));
47 | await super.close();
48 | _subscriptions.clear();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/core/utils/periodic_task_executor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | class PeriodicTaskExecutor {
4 | Timer? _timer;
5 | bool Function() _shouldStopCallback = () => false;
6 |
7 | /// runs the given [task] periodically
8 | ///
9 | /// params:
10 | /// [shouldStop] - callback to check whether the executor should stop running the periodic task, called just after
11 | /// each invocation of [task]
12 | /// [period] - how often to run the task
13 | /// [runOnStart] - whether to run the task immediately after start or after the [period] passes
14 | void start({
15 | bool runOnStart = true,
16 | required Duration period,
17 | required FutureOr Function() task,
18 | bool Function()? shouldStop,
19 | }) {
20 | _shouldStopCallback = shouldStop ?? () => false;
21 | if (runOnStart) {
22 | task();
23 | }
24 | _timer = Timer.periodic(
25 | period,
26 | (timer) async {
27 | await task();
28 | if (_shouldStopCallback()) {
29 | cancel();
30 | }
31 | },
32 | );
33 | }
34 |
35 | /// cancels the periodic task
36 | void cancel() => _timer?.cancel();
37 | }
38 |
--------------------------------------------------------------------------------
/lib/dependency_injection/app_component.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/core/domain/stores/user_store.dart';
2 | import 'package:flutter_demo/features/app_init/dependency_injection/feature_component.dart' as app_init;
3 | import 'package:flutter_demo/features/auth/dependency_injection/feature_component.dart' as auth;
4 | import 'package:flutter_demo/navigation/app_navigator.dart';
5 | import 'package:get_it/get_it.dart';
6 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS
7 |
8 | final getIt = GetIt.instance;
9 |
10 | /// registers all the dependencies in dependency graph in get_it package
11 | void configureDependencies() {
12 | app_init.configureDependencies();
13 | auth.configureDependencies();
14 | //DO-NOT-REMOVE FEATURE_COMPONENT_INIT
15 |
16 | _configureGeneralDependencies();
17 | _configureRepositories();
18 | _configureStores();
19 | _configureUseCases();
20 | _configureMvp();
21 | }
22 |
23 | //ignore: long-method
24 | void _configureGeneralDependencies() {
25 | // ignore: unnecessary_statements
26 | getIt
27 | ..registerFactory(
28 | () => AppNavigator(),
29 | )
30 | //DO-NOT-REMOVE GENERAL_DEPS_GET_IT_CONFIG
31 | ;
32 | }
33 |
34 | //ignore: long-method
35 | void _configureRepositories() {
36 | // ignore: unnecessary_statements
37 | getIt
38 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG
39 | ;
40 | }
41 |
42 | //ignore: long-method
43 | void _configureStores() {
44 | // ignore: unnecessary_statements
45 | getIt
46 | ..registerFactory(
47 | () => UserStore(),
48 | )
49 | //DO-NOT-REMOVE STORES_GET_IT_CONFIG
50 | ;
51 | }
52 |
53 | //ignore: long-method
54 | void _configureUseCases() {
55 | // ignore: unnecessary_statements
56 | getIt
57 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG
58 | ;
59 | }
60 |
61 | //ignore: long-method
62 | void _configureMvp() {
63 | // ignore: unnecessary_statements
64 | getIt
65 | //DO-NOT-REMOVE MVP_GET_IT_CONFIG
66 | ;
67 | }
68 |
--------------------------------------------------------------------------------
/lib/features/app_init/app_init_initial_params.dart:
--------------------------------------------------------------------------------
1 | class AppInitInitialParams {
2 | const AppInitInitialParams();
3 | }
4 |
--------------------------------------------------------------------------------
/lib/features/app_init/app_init_navigator.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/navigation/app_navigator.dart';
2 | import 'package:flutter_demo/navigation/error_dialog_route.dart';
3 | import 'package:flutter_demo/navigation/no_routes.dart';
4 |
5 | class AppInitNavigator with NoRoutes, ErrorDialogRoute {
6 | AppInitNavigator(this.appNavigator);
7 |
8 | @override
9 | final AppNavigator appNavigator;
10 | }
11 |
--------------------------------------------------------------------------------
/lib/features/app_init/app_init_page.dart:
--------------------------------------------------------------------------------
1 | // ignore: unused_import
2 | import 'package:bloc/bloc.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_demo/core/utils/mvp_extensions.dart';
5 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart';
6 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart';
7 | import 'package:flutter_demo/resources/assets.gen.dart';
8 |
9 | class AppInitPage extends StatefulWidget with HasPresenter {
10 | const AppInitPage({
11 | required this.presenter,
12 | super.key,
13 | });
14 |
15 | @override
16 | final AppInitPresenter presenter;
17 |
18 | @override
19 | State createState() => _AppInitPageState();
20 | }
21 |
22 | class _AppInitPageState extends State
23 | with PresenterStateMixin {
24 | @override
25 | void initState() {
26 | super.initState();
27 | presenter.onInit();
28 | }
29 |
30 | @override
31 | Widget build(BuildContext context) => Scaffold(
32 | body: stateObserver(
33 | builder: (context, state) => Padding(
34 | padding: const EdgeInsets.all(32.0),
35 | child: Column(
36 | mainAxisAlignment: MainAxisAlignment.center,
37 | children: [
38 | Assets.images.logo.image(),
39 | const SizedBox(height: 16),
40 | if (state.isLoading) const CircularProgressIndicator(),
41 | ],
42 | ),
43 | ),
44 | ),
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/lib/features/app_init/app_init_presentation_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_demo/core/domain/model/app_init_failure.dart';
3 | import 'package:flutter_demo/core/domain/model/user.dart';
4 | import 'package:flutter_demo/core/utils/bloc_extensions.dart';
5 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart';
6 |
7 | /// Model used by presenter, contains fields that are relevant to presenters and implements ViewModel to expose data to view (page)
8 | class AppInitPresentationModel implements AppInitViewModel {
9 | /// Creates the initial state
10 | AppInitPresentationModel.initial(
11 | // ignore: avoid_unused_constructor_parameters
12 | AppInitInitialParams initialParams,
13 | ) : appInitResult = const FutureResult.empty(),
14 | user = const User.anonymous();
15 |
16 | /// Used for the copyWith method
17 | AppInitPresentationModel._({
18 | required this.appInitResult,
19 | required this.user,
20 | });
21 |
22 | final FutureResult> appInitResult;
23 | final User user;
24 |
25 | @override
26 | bool get isLoading => appInitResult.isPending();
27 |
28 | AppInitPresentationModel copyWith({
29 | FutureResult>? appInitResult,
30 | User? user,
31 | }) =>
32 | AppInitPresentationModel._(
33 | appInitResult: appInitResult ?? this.appInitResult,
34 | user: user ?? this.user,
35 | );
36 | }
37 |
38 | /// Interface to expose fields used by the view (page).
39 | abstract class AppInitViewModel {
40 | bool get isLoading;
41 | }
42 |
--------------------------------------------------------------------------------
/lib/features/app_init/app_init_presenter.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:bloc/bloc.dart';
4 | import 'package:flutter_demo/core/domain/model/user.dart';
5 | import 'package:flutter_demo/core/domain/stores/user_store.dart';
6 | import 'package:flutter_demo/core/domain/use_cases/app_init_use_case.dart';
7 | import 'package:flutter_demo/core/helpers.dart';
8 | import 'package:flutter_demo/core/utils/bloc_extensions.dart';
9 | import 'package:flutter_demo/core/utils/either_extensions.dart';
10 | import 'package:flutter_demo/core/utils/mvp_extensions.dart';
11 | import 'package:flutter_demo/features/app_init/app_init_navigator.dart';
12 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart';
13 |
14 | class AppInitPresenter extends Cubit with CubitToCubitCommunicationMixin {
15 | AppInitPresenter(
16 | AppInitPresentationModel super.model,
17 | this.navigator,
18 | this.appInitUseCase,
19 | this.userStore,
20 | ) {
21 | listenTo(
22 | userStore,
23 | onChange: (user) => emit(_model.copyWith(user: user)),
24 | );
25 | }
26 |
27 | final AppInitNavigator navigator;
28 | final AppInitUseCase appInitUseCase;
29 | final UserStore userStore;
30 |
31 | AppInitPresentationModel get _model => state as AppInitPresentationModel;
32 |
33 | Future onInit() async {
34 | await appInitUseCase
35 | .execute() //
36 | .observeStatusChanges((result) => emit(_model.copyWith(appInitResult: result)))
37 | .asyncFold(
38 | (fail) => navigator.showError(fail.displayableFailure()),
39 | (success) => doNothing(), //todo!
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/features/app_init/dependency_injection/feature_component.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/core/domain/use_cases/app_init_use_case.dart';
2 | import 'package:flutter_demo/dependency_injection/app_component.dart';
3 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart';
4 | import 'package:flutter_demo/features/app_init/app_init_navigator.dart';
5 | import 'package:flutter_demo/features/app_init/app_init_page.dart';
6 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart';
7 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart';
8 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS
9 |
10 | /// registers all the dependencies in dependency graph in get_it package
11 | void configureDependencies() {
12 | _configureGeneralDependencies();
13 | _configureRepositories();
14 | _configureStores();
15 | _configureUseCases();
16 | _configureMvp();
17 | }
18 |
19 | //ignore: long-method
20 | void _configureGeneralDependencies() {
21 | // ignore: unnecessary_statements
22 | getIt
23 | //DO-NOT-REMOVE GENERAL_DEPS_GET_IT_CONFIG
24 | ;
25 | }
26 |
27 | //ignore: long-method
28 | void _configureRepositories() {
29 | // ignore: unnecessary_statements
30 | getIt
31 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG
32 | ;
33 | }
34 |
35 | //ignore: long-method
36 | void _configureStores() {
37 | // ignore: unnecessary_statements
38 | getIt
39 | //DO-NOT-REMOVE STORES_GET_IT_CONFIG
40 | ;
41 | }
42 |
43 | //ignore: long-method
44 | void _configureUseCases() {
45 | // ignore: unnecessary_statements
46 | getIt
47 | ..registerFactory(
48 | () => const AppInitUseCase(),
49 | )
50 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG
51 | ;
52 | }
53 |
54 | //ignore: long-method
55 | void _configureMvp() {
56 | // ignore: unnecessary_statements
57 | getIt
58 | ..registerFactory(
59 | () => AppInitNavigator(getIt()),
60 | )
61 | ..registerFactoryParam(
62 | (params, _) => AppInitPresentationModel.initial(params),
63 | )
64 | ..registerFactoryParam(
65 | (initialParams, _) => AppInitPresenter(
66 | getIt(param1: initialParams),
67 | getIt(),
68 | getIt(),
69 | getIt(),
70 | ),
71 | )
72 | ..registerFactoryParam(
73 | (initialParams, _) => AppInitPage(
74 | presenter: getIt(param1: initialParams),
75 | ),
76 | )
77 | //DO-NOT-REMOVE MVP_GET_IT_CONFIG
78 | ;
79 | }
80 |
--------------------------------------------------------------------------------
/lib/features/auth/dependency_injection/feature_component.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/dependency_injection/app_component.dart';
2 | import 'package:flutter_demo/features/auth/domain/use_cases/log_in_use_case.dart';
3 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart';
4 | import 'package:flutter_demo/features/auth/login/login_navigator.dart';
5 | import 'package:flutter_demo/features/auth/login/login_page.dart';
6 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart';
7 | import 'package:flutter_demo/features/auth/login/login_presenter.dart';
8 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS
9 |
10 | /// registers all the dependencies in dependency graph in get_it package
11 | void configureDependencies() {
12 | _configureGeneralDependencies();
13 | _configureRepositories();
14 | _configureStores();
15 | _configureUseCases();
16 | _configureMvp();
17 | }
18 |
19 | //ignore: long-method
20 | void _configureGeneralDependencies() {
21 | // ignore: unnecessary_statements
22 | getIt
23 | //DO-NOT-REMOVE GENERAL_DEPS_GET_IT_CONFIG
24 | ;
25 | }
26 |
27 | //ignore: long-method
28 | void _configureRepositories() {
29 | // ignore: unnecessary_statements
30 | getIt
31 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG
32 | ;
33 | }
34 |
35 | //ignore: long-method
36 | void _configureStores() {
37 | // ignore: unnecessary_statements
38 | getIt
39 | //DO-NOT-REMOVE STORES_GET_IT_CONFIG
40 | ;
41 | }
42 |
43 | //ignore: long-method
44 | void _configureUseCases() {
45 | // ignore: unnecessary_statements
46 | getIt
47 | ..registerFactory(
48 | () => LogInUseCase(
49 | getIt(),
50 | ),
51 | )
52 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG
53 |
54 | ;
55 | }
56 |
57 | //ignore: long-method
58 | void _configureMvp() {
59 | // ignore: unnecessary_statements
60 | getIt
61 | ..registerFactory(
62 | () => LoginNavigator(getIt()),
63 | )
64 | ..registerFactoryParam(
65 | (params, _) => LoginPresentationModel.initial(params),
66 | )
67 | ..registerFactoryParam(
68 | (initialParams, _) => LoginPresenter(
69 | getIt(param1: initialParams),
70 | getIt(),
71 | ),
72 | )
73 | ..registerFactoryParam(
74 | (initialParams, _) => LoginPage(
75 | presenter: getIt(param1: initialParams),
76 | ),
77 | )
78 | //DO-NOT-REMOVE MVP_GET_IT_CONFIG
79 |
80 | ;
81 | }
82 |
--------------------------------------------------------------------------------
/lib/features/auth/domain/model/log_in_failure.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/core/domain/model/displayable_failure.dart';
2 | import 'package:flutter_demo/localization/app_localizations_utils.dart';
3 |
4 | class LogInFailure implements HasDisplayableFailure {
5 | // ignore: avoid_field_initializers_in_const_classes
6 | const LogInFailure.unknown([this.cause]) : type = LogInFailureType.unknown;
7 |
8 | const LogInFailure.missingCredentials([this.cause]) : type = LogInFailureType.missingCredentials;
9 |
10 | final LogInFailureType type;
11 | final Object? cause;
12 |
13 | @override
14 | DisplayableFailure displayableFailure() {
15 | switch (type) {
16 | case LogInFailureType.unknown:
17 | return DisplayableFailure.commonError();
18 | case LogInFailureType.missingCredentials:
19 | return DisplayableFailure(
20 | title: appLocalizations.missingCredsTitle,
21 | message: appLocalizations.missingCredsMessage,
22 | );
23 | }
24 | }
25 |
26 | @override
27 | String toString() => 'LogInFailure{type: $type, cause: $cause}';
28 | }
29 |
30 | enum LogInFailureType {
31 | unknown,
32 | missingCredentials,
33 | }
34 |
--------------------------------------------------------------------------------
/lib/features/auth/domain/use_cases/log_in_use_case.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:dartz/dartz.dart';
4 | import 'package:flutter_demo/core/domain/model/user.dart';
5 | import 'package:flutter_demo/core/domain/stores/user_store.dart';
6 | import 'package:flutter_demo/core/utils/either_extensions.dart';
7 | import 'package:flutter_demo/features/auth/domain/model/log_in_failure.dart';
8 | import 'package:flutter_demo/main.dart';
9 |
10 | class LogInUseCase {
11 | const LogInUseCase(this._userStore);
12 |
13 | final UserStore _userStore;
14 |
15 | Future> execute({
16 | required String username,
17 | required String password,
18 | }) async {
19 | if (username.isEmpty || password.isEmpty) {
20 | return failure(const LogInFailure.missingCredentials());
21 | }
22 |
23 | if (!isUnitTests) {
24 | //TODO simulation of network request
25 | //ignore: no-magic-number
26 | await Future.delayed(Duration(milliseconds: 500 + Random().nextInt(1000)));
27 | }
28 |
29 | if (username == 'test' && password == 'test123') {
30 | final user = User(
31 | id: "id_$username",
32 | username: username,
33 | );
34 | _userStore.user = user;
35 | return success(
36 | user,
37 | );
38 | }
39 | return failure(const LogInFailure.unknown());
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/features/auth/login/login_initial_params.dart:
--------------------------------------------------------------------------------
1 | class LoginInitialParams {
2 | const LoginInitialParams();
3 | }
4 |
--------------------------------------------------------------------------------
/lib/features/auth/login/login_navigator.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/dependency_injection/app_component.dart';
2 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart';
3 | import 'package:flutter_demo/features/auth/login/login_page.dart';
4 | import 'package:flutter_demo/navigation/app_navigator.dart';
5 | import 'package:flutter_demo/navigation/no_routes.dart';
6 |
7 | class LoginNavigator with NoRoutes {
8 | LoginNavigator(this.appNavigator);
9 |
10 | @override
11 | final AppNavigator appNavigator;
12 | }
13 |
14 | //ignore: unused-code
15 | mixin LoginRoute {
16 | Future openLogin(LoginInitialParams initialParams) async {
17 | return appNavigator.push(
18 | materialRoute(getIt(param1: initialParams)),
19 | );
20 | }
21 |
22 | AppNavigator get appNavigator;
23 | }
24 |
--------------------------------------------------------------------------------
/lib/features/auth/login/login_page.dart:
--------------------------------------------------------------------------------
1 | // ignore: unused_import
2 | import 'package:bloc/bloc.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_demo/core/helpers.dart';
5 | import 'package:flutter_demo/core/utils/mvp_extensions.dart';
6 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart';
7 | import 'package:flutter_demo/features/auth/login/login_presenter.dart';
8 | import 'package:flutter_demo/localization/app_localizations_utils.dart';
9 |
10 | class LoginPage extends StatefulWidget with HasPresenter {
11 | const LoginPage({
12 | required this.presenter,
13 | super.key,
14 | });
15 |
16 | @override
17 | final LoginPresenter presenter;
18 |
19 | @override
20 | State createState() => _LoginPageState();
21 | }
22 |
23 | class _LoginPageState extends State with PresenterStateMixin {
24 | @override
25 | Widget build(BuildContext context) => Scaffold(
26 | body: Padding(
27 | padding: const EdgeInsets.all(32.0),
28 | child: Column(
29 | mainAxisAlignment: MainAxisAlignment.center,
30 | children: [
31 | TextField(
32 | decoration: InputDecoration(
33 | hintText: appLocalizations.usernameHint,
34 | ),
35 | onChanged: (text) => doNothing(), //TODO
36 | ),
37 | const SizedBox(height: 8),
38 | TextField(
39 | obscureText: true,
40 | decoration: InputDecoration(
41 | hintText: appLocalizations.passwordHint,
42 | ),
43 | onChanged: (text) => doNothing(), //TODO
44 | ),
45 | const SizedBox(height: 16),
46 | stateObserver(
47 | builder: (context, state) => ElevatedButton(
48 | onPressed: () => doNothing(), //TODO
49 | child: Text(appLocalizations.logInAction),
50 | ),
51 | ),
52 | ],
53 | ),
54 | ),
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/lib/features/auth/login/login_presentation_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart';
2 |
3 | /// Model used by presenter, contains fields that are relevant to presenters and implements ViewModel to expose data to view (page)
4 | class LoginPresentationModel implements LoginViewModel {
5 | /// Creates the initial state
6 | LoginPresentationModel.initial(
7 | // ignore: avoid_unused_constructor_parameters
8 | LoginInitialParams initialParams,
9 | );
10 |
11 | /// Used for the copyWith method
12 | LoginPresentationModel._();
13 |
14 | LoginPresentationModel copyWith() {
15 | return LoginPresentationModel._();
16 | }
17 | }
18 |
19 | /// Interface to expose fields used by the view (page).
20 | abstract class LoginViewModel {}
21 |
--------------------------------------------------------------------------------
/lib/features/auth/login/login_presenter.dart:
--------------------------------------------------------------------------------
1 | import 'package:bloc/bloc.dart';
2 | import 'package:flutter_demo/features/auth/login/login_navigator.dart';
3 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart';
4 |
5 | class LoginPresenter extends Cubit {
6 | LoginPresenter(
7 | LoginPresentationModel super.model,
8 | this.navigator,
9 | );
10 |
11 | final LoginNavigator navigator;
12 |
13 | // ignore: unused_element
14 | LoginPresentationModel get _model => state as LoginPresentationModel;
15 | }
16 |
--------------------------------------------------------------------------------
/lib/flutter_demo_app.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_demo/dependency_injection/app_component.dart';
3 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart';
4 | import 'package:flutter_demo/features/app_init/app_init_page.dart';
5 | import 'package:flutter_demo/navigation/app_navigator.dart';
6 | import 'package:flutter_demo/utils/locale_resolution.dart';
7 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
8 | import 'package:flutter_localizations/flutter_localizations.dart';
9 |
10 | class FlutterDemoApp extends StatefulWidget {
11 | const FlutterDemoApp({super.key});
12 |
13 | @override
14 | State createState() => _FlutterDemoAppState();
15 | }
16 |
17 | class _FlutterDemoAppState extends State {
18 | late AppInitPage page;
19 |
20 | @override
21 | void initState() {
22 | page = getIt(param1: const AppInitInitialParams());
23 | super.initState();
24 | }
25 |
26 | @override
27 | Widget build(BuildContext context) {
28 | return MaterialApp(
29 | home: page,
30 | debugShowCheckedModeBanner: false,
31 | navigatorKey: AppNavigator.navigatorKey,
32 | localizationsDelegates: const [
33 | AppLocalizations.delegate,
34 | GlobalMaterialLocalizations.delegate,
35 | GlobalWidgetsLocalizations.delegate,
36 | GlobalCupertinoLocalizations.delegate,
37 | ],
38 | localeListResolutionCallback: localeResolution,
39 | supportedLocales: AppLocalizations.supportedLocales,
40 | builder: (context, child) => MediaQuery(
41 | data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
42 | child: child!,
43 | ),
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/l10n/intl_en.arb:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/lib/localization/app_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "@@locale": "en",
3 | "appTitle": "Flutter Demo",
4 | "okAction": "Ok",
5 | "commonErrorTitle": "Error",
6 | "commonErrorMessage": "Something went wrong, please try again",
7 | "missingCredsTitle": "Missing Credentials",
8 | "missingCredsMessage": "Please provide both username and password!",
9 | "usernameHint": "username",
10 | "passwordHint": "password",
11 | "logInAction": "Log in"
12 | }
--------------------------------------------------------------------------------
/lib/localization/app_localizations_utils.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/navigation/app_navigator.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 |
4 | AppLocalizations? _appLocalizations;
5 |
6 | /// Convenience getter for the app localizations
7 | AppLocalizations get appLocalizations {
8 | _appLocalizations ??= AppLocalizations.of(AppNavigator.navigatorKey.currentContext!)!;
9 | return _appLocalizations!;
10 | }
11 |
12 | /// Useful method for tests to override app localizations
13 | void overrideAppLocalizations(AppLocalizations localizations) {
14 | _appLocalizations = localizations;
15 | }
16 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: unused-code
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_demo/core/helpers.dart';
4 | import 'package:flutter_demo/dependency_injection/app_component.dart';
5 | import 'package:flutter_demo/flutter_demo_app.dart';
6 | import 'package:flutter_demo/navigation/close_with_result_route.dart';
7 |
8 | /// flag modified by unit tests so that app's code can adapt to unit tests
9 | /// (i.e: disable animations in progress bars etc.)
10 | bool isUnitTests = false;
11 |
12 | void main() {
13 | configureDependencies();
14 | _suppressUnusedCodeWarnings(); // used in tests
15 | runApp(const FlutterDemoApp());
16 | }
17 |
18 | /// hacky way to get rid of false-positive `unused-code` warnings from dart_code_metrics
19 | /// https://github.com/dart-code-checker/dart-code-metrics/pull/929
20 | void _suppressUnusedCodeWarnings() {
21 | suppressUnusedCodeWarning([
22 | notImplemented,
23 | CloseWithResultRoute,
24 | ]);
25 | }
26 |
--------------------------------------------------------------------------------
/lib/navigation/alert_dialog_route.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_demo/localization/app_localizations_utils.dart';
3 | import 'package:flutter_demo/navigation/app_navigator.dart';
4 |
5 | //ignore: unused-code
6 | mixin AlertDialogRoute {
7 | Future showAlert({
8 | required String title,
9 | required String message,
10 | }) {
11 | return showDialog(
12 | context: AppNavigator.navigatorKey.currentContext!,
13 | builder: (context) => _AlertDialog(
14 | title: title,
15 | message: message,
16 | ),
17 | );
18 | }
19 | }
20 |
21 | class _AlertDialog extends StatelessWidget {
22 | const _AlertDialog({
23 | required this.title,
24 | required this.message,
25 | });
26 |
27 | final String title;
28 | final String message;
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | return AlertDialog(
33 | title: Text(title),
34 | content: Text(message),
35 | actions: [
36 | TextButton(
37 | onPressed: () => Navigator.of(context).pop(),
38 | child: Text(appLocalizations.okAction),
39 | ),
40 | ],
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/navigation/app_navigator.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_demo/core/helpers.dart';
3 | import 'package:flutter_demo/core/utils/durations.dart';
4 | import 'package:flutter_demo/navigation/transitions/fade_in_page_transition.dart';
5 | import 'package:flutter_demo/navigation/transitions/slide_bottom_page_transition.dart';
6 |
7 | class AppNavigator {
8 | AppNavigator() {
9 | suppressUnusedCodeWarning([fadeInRoute, slideBottomRoute]);
10 | }
11 |
12 | static final navigatorKey = GlobalKey();
13 |
14 | Future push(
15 | Route route, {
16 | BuildContext? context,
17 | bool useRoot = false,
18 | }) async {
19 | return _navigator(context, useRoot: useRoot).push(route);
20 | }
21 |
22 | Future pushReplacement(
23 | Route route, {
24 | BuildContext? context,
25 | bool useRoot = false,
26 | }) async {
27 | return _navigator(context, useRoot: useRoot).pushReplacement(route);
28 | }
29 |
30 | void close({
31 | BuildContext? context,
32 | }) =>
33 | closeWithResult(null, context: context);
34 |
35 | void closeWithResult(
36 | T result, {
37 | BuildContext? context,
38 | }) =>
39 | _navigator(context).canPop() ? _navigator(context).pop(result) : result;
40 |
41 | void popUntilRoot(BuildContext context) => _navigator(context).popUntil((route) => route.isFirst);
42 |
43 | void popUntilPageWithName(
44 | String title, {
45 | BuildContext? context,
46 | }) =>
47 | _navigator(context).popUntil(ModalRoute.withName(title));
48 | }
49 |
50 | //ignore: long-parameter-list
51 | Route fadeInRoute(
52 | Widget page, {
53 | int? durationMillis,
54 | String? pageName,
55 | bool opaque = true,
56 | bool fadeOut = true,
57 | }) =>
58 | PageRouteBuilder(
59 | opaque: opaque,
60 | transitionDuration: Duration(
61 | milliseconds: durationMillis ?? Durations.medium,
62 | ),
63 | settings: RouteSettings(name: pageName ?? page.runtimeType.toString()),
64 | pageBuilder: _pageBuilder(page),
65 | transitionsBuilder: fadeInPageTransition(fadeOut: fadeOut),
66 | );
67 |
68 | //ignore: long-parameter-list, unused-code
69 | Route noTransitionRoute(
70 | Widget page, {
71 | int? durationMillis,
72 | String? pageName,
73 | bool opaque = true,
74 | }) =>
75 | PageRouteBuilder(
76 | opaque: opaque,
77 | transitionDuration: Duration.zero,
78 | settings: RouteSettings(name: pageName ?? page.runtimeType.toString()),
79 | pageBuilder: _pageBuilder(page),
80 | );
81 |
82 | Route materialRoute(
83 | Widget page, {
84 | bool fullScreenDialog = false,
85 | String? pageName,
86 | }) =>
87 | MaterialPageRoute(
88 | builder: (context) => page,
89 | settings: RouteSettings(name: pageName ?? page.runtimeType.toString()),
90 | fullscreenDialog: fullScreenDialog,
91 | );
92 |
93 | //ignore: long-parameter-list
94 | Route slideBottomRoute(
95 | Widget page, {
96 | int? durationMillis,
97 | bool fullScreenDialog = false,
98 | String? pageName,
99 | bool opaque = true,
100 | }) =>
101 | PageRouteBuilder(
102 | opaque: opaque,
103 | transitionDuration: Duration(
104 | milliseconds: durationMillis ?? Durations.medium,
105 | ),
106 | fullscreenDialog: fullScreenDialog,
107 | settings: RouteSettings(name: pageName ?? page.runtimeType.toString()),
108 | pageBuilder: _pageBuilder(page),
109 | transitionsBuilder: slideBottomPageTransition(),
110 | );
111 |
112 | RoutePageBuilder _pageBuilder(Widget page) => (
113 | context,
114 | animation,
115 | secondaryAnimation,
116 | ) =>
117 | page;
118 |
119 | NavigatorState _navigator(BuildContext? context, {bool useRoot = false}) =>
120 | (useRoot || context == null) ? AppNavigator.navigatorKey.currentState! : Navigator.of(context);
121 |
--------------------------------------------------------------------------------
/lib/navigation/close_route.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: unused-code, unused-files
2 | import 'package:flutter_demo/navigation/app_navigator.dart';
3 |
4 | mixin CloseRoute {
5 | AppNavigator get appNavigator;
6 |
7 | void close() => appNavigator.close();
8 | }
9 |
--------------------------------------------------------------------------------
/lib/navigation/close_with_result_route.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/navigation/app_navigator.dart';
2 |
3 | mixin CloseWithResultRoute {
4 | AppNavigator get appNavigator;
5 |
6 | void closeWithResult(T? result) => appNavigator.closeWithResult(result);
7 | }
8 |
--------------------------------------------------------------------------------
/lib/navigation/error_dialog_route.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_demo/core/domain/model/displayable_failure.dart';
3 | import 'package:flutter_demo/core/utils/logging.dart';
4 | import 'package:flutter_demo/localization/app_localizations_utils.dart';
5 | import 'package:flutter_demo/navigation/app_navigator.dart';
6 |
7 | mixin ErrorDialogRoute {
8 | Future showError(DisplayableFailure failure, {BuildContext? context}) {
9 | logError(failure);
10 | return showDialog(
11 | context: context ?? AppNavigator.navigatorKey.currentContext!,
12 | builder: (context) => ErrorDialog(failure: failure),
13 | );
14 | }
15 | }
16 |
17 | class ErrorDialog extends StatelessWidget {
18 | const ErrorDialog({
19 | required this.failure,
20 | super.key,
21 | });
22 |
23 | final DisplayableFailure failure;
24 |
25 | @override
26 | Widget build(BuildContext context) {
27 | return AlertDialog(
28 | title: Text(failure.title),
29 | content: Text(failure.message),
30 | actions: [
31 | TextButton(
32 | onPressed: () => Navigator.of(context).pop(),
33 | child: Text(appLocalizations.okAction),
34 | ),
35 | ],
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/navigation/no_routes.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/navigation/app_navigator.dart';
2 |
3 | /// used with navigators that don't have any routes (yet).
4 | mixin NoRoutes {
5 | AppNavigator get appNavigator;
6 | }
7 |
--------------------------------------------------------------------------------
/lib/navigation/transitions/fade_in_page_transition.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | RouteTransitionsBuilder fadeInPageTransition({
4 | required bool fadeOut,
5 | }) =>
6 | (
7 | context,
8 | animation,
9 | secondaryAnimation,
10 | child,
11 | ) =>
12 | FadeTransition(
13 | opacity: Tween(begin: 0, end: 1).animate(animation),
14 | child: fadeOut
15 | ? FadeTransition(
16 | opacity: Tween(begin: 1, end: 0).animate(secondaryAnimation),
17 | child: child,
18 | )
19 | : child,
20 | );
21 |
--------------------------------------------------------------------------------
/lib/navigation/transitions/slide_bottom_page_transition.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | RouteTransitionsBuilder slideBottomPageTransition() => (
4 | context,
5 | animation,
6 | secondaryAnimation,
7 | child,
8 | ) =>
9 | SlideTransition(
10 | position: Tween(
11 | begin: const Offset(0.0, 1.0),
12 | end: Offset.zero,
13 | ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutQuint)),
14 | child: child,
15 | );
16 |
--------------------------------------------------------------------------------
/lib/resources/assets.gen.dart:
--------------------------------------------------------------------------------
1 | /// GENERATED CODE - DO NOT MODIFY BY HAND
2 | /// *****************************************************
3 | /// FlutterGen
4 | /// *****************************************************
5 |
6 | // coverage:ignore-file
7 | // ignore_for_file: type=lint
8 | // ignore_for_file: directives_ordering,unnecessary_import
9 |
10 | import 'package:flutter/widgets.dart';
11 |
12 | class $AssetsImagesGen {
13 | const $AssetsImagesGen();
14 |
15 | /// File path: assets/images/logo.webp
16 | AssetGenImage get logo => const AssetGenImage('assets/images/logo.webp');
17 | }
18 |
19 | class Assets {
20 | Assets._();
21 |
22 | static const $AssetsImagesGen images = $AssetsImagesGen();
23 | }
24 |
25 | class AssetGenImage {
26 | const AssetGenImage(this._assetName);
27 |
28 | final String _assetName;
29 |
30 | Image image({
31 | Key? key,
32 | AssetBundle? bundle,
33 | ImageFrameBuilder? frameBuilder,
34 | ImageErrorWidgetBuilder? errorBuilder,
35 | String? semanticLabel,
36 | bool excludeFromSemantics = false,
37 | double? scale,
38 | double? width,
39 | double? height,
40 | Color? color,
41 | Animation? opacity,
42 | BlendMode? colorBlendMode,
43 | BoxFit? fit,
44 | AlignmentGeometry alignment = Alignment.center,
45 | ImageRepeat repeat = ImageRepeat.noRepeat,
46 | Rect? centerSlice,
47 | bool matchTextDirection = false,
48 | bool gaplessPlayback = false,
49 | bool isAntiAlias = false,
50 | String? package,
51 | FilterQuality filterQuality = FilterQuality.low,
52 | int? cacheWidth,
53 | int? cacheHeight,
54 | }) {
55 | return Image.asset(
56 | _assetName,
57 | key: key,
58 | bundle: bundle,
59 | frameBuilder: frameBuilder,
60 | errorBuilder: errorBuilder,
61 | semanticLabel: semanticLabel,
62 | excludeFromSemantics: excludeFromSemantics,
63 | scale: scale,
64 | width: width,
65 | height: height,
66 | color: color,
67 | opacity: opacity,
68 | colorBlendMode: colorBlendMode,
69 | fit: fit,
70 | alignment: alignment,
71 | repeat: repeat,
72 | centerSlice: centerSlice,
73 | matchTextDirection: matchTextDirection,
74 | gaplessPlayback: gaplessPlayback,
75 | isAntiAlias: isAntiAlias,
76 | package: package,
77 | filterQuality: filterQuality,
78 | cacheWidth: cacheWidth,
79 | cacheHeight: cacheHeight,
80 | );
81 | }
82 |
83 | String get path => _assetName;
84 |
85 | String get keyName => _assetName;
86 | }
87 |
--------------------------------------------------------------------------------
/lib/utils/locale_resolution.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/rendering.dart';
2 |
3 | Locale localeResolution(
4 | List? locales,
5 | Iterable supportedLocales,
6 | ) =>
7 | (locales ?? []).firstWhere(
8 | (locale) => supportedLocales.any(
9 | (supported) => supported.languageCode == locale.languageCode,
10 | ),
11 | orElse: () => const Locale('en'),
12 | );
13 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: flutter_demo
2 | description: A new Flutter project.
3 |
4 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev
5 | version: 1.0.0+1
6 |
7 | environment:
8 | sdk: ">=2.17.0 <3.0.0"
9 |
10 | dependencies:
11 | flutter:
12 | sdk: flutter
13 | flutter_localizations:
14 | sdk: flutter
15 |
16 | # architecture
17 | bloc: 8.0.3
18 | flutter_bloc: 8.0.1
19 |
20 | # dependency injection
21 | get_it: 7.2.0
22 |
23 | # functional programming, used for Either type
24 | dartz: 0.10.1
25 |
26 | # equality checks
27 | equatable: 2.0.3
28 |
29 | # localization
30 | intl: 0.17.0
31 |
32 | # widgets
33 | gap: 2.0.0
34 |
35 |
36 | dev_dependencies:
37 | flutter_test:
38 | sdk: flutter
39 |
40 | # code analysis
41 | lint: 1.10.0
42 | dart_code_metrics: 4.17.0
43 | custom_lint:
44 | git:
45 | url: https://github.com/andrzejchm/dart_custom_lint.git
46 | path: packages/custom_lint
47 | ref: main
48 | clean_architecture_lints:
49 | path: tools/custom_lints/clean_architecture_lints
50 |
51 | # tests
52 | golden_toolkit: 0.13.0
53 | alchemist: 0.4.1
54 | mocktail_image_network: 0.3.1
55 | mocktail: 0.3.0
56 | bloc_test: 9.0.3
57 |
58 |
59 |
60 |
61 | flutter:
62 | uses-material-design: true
63 | generate: true
64 |
65 | assets:
66 | - assets/
67 | - assets/images/
68 |
69 | flutter_gen:
70 | output: lib/resources/
71 | line_length: 120
72 |
73 | flutter_intl:
74 | enabled: true
75 |
--------------------------------------------------------------------------------
/templates/mason-lock.json:
--------------------------------------------------------------------------------
1 | {"bricks":{"use_case":{"path":"use_case"},"page":{"path":"page"},"repository":{"path":"repository"}}}
--------------------------------------------------------------------------------
/templates/mason.yaml:
--------------------------------------------------------------------------------
1 | bricks:
2 | use_case:
3 | path: use_case
4 | page:
5 | path: page
6 | repository:
7 | path: repository
--------------------------------------------------------------------------------
/templates/page/README.md:
--------------------------------------------------------------------------------
1 | # page
2 |
3 | [](https://github.com/felangel/mason)
4 |
5 | Creates the MVP (Model-View-Presenter) structure for a screen
6 |
7 | creates following files:
8 |
9 | ```
10 | - lib/features/$FEATURE_NAME$/$NAME$_page.dart
11 | - lib/features/$FEATURE_NAME$/$NAME$_presenter.dart
12 | - lib/features/$FEATURE_NAME$/$NAME$_presentation_model.dart
13 | - lib/features/$FEATURE_NAME$/$NAME$_initial_params.dart
14 | - lib/features/$FEATURE_NAME$/$NAME$_navigator.dart
15 |
16 | - test/pages/$NAME$_page_test.dart
17 | - test/presenters/$NAME$_presenter_test.dart
18 | ```
19 |
20 | also modifies following files
21 |
22 | - `app_component.dart` - adds getIt registration for the page and its dependencies
23 | - `test/mock_definitions.dart` - adds mock definitions for the MVP classes
24 | - `test/mocks.dart` - creates the mocks as static fields for use in tests
--------------------------------------------------------------------------------
/templates/page/__brick__/{{{initial_params_absolute_path}}}:
--------------------------------------------------------------------------------
1 | class {{initial_params_name}} {
2 | const {{initial_params_name}}();
3 | }
4 |
--------------------------------------------------------------------------------
/templates/page/__brick__/{{{navigator_absolute_path}}}:
--------------------------------------------------------------------------------
1 | import 'package:{{{app_package}}}/dependency_injection/app_component.dart';
2 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{initial_params_file_name}}}';
3 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{page_file_name}}}';
4 | import 'package:{{{app_package}}}/navigation/app_navigator.dart';
5 | import 'package:{{{app_package}}}/navigation/no_routes.dart';
6 |
7 | class {{navigator_name}} with NoRoutes {
8 |
9 | {{navigator_name}}(this.appNavigator);
10 |
11 | @override
12 | final AppNavigator appNavigator;
13 | }
14 |
15 | mixin {{route_name}} {
16 | Future open{{stem}}({{initial_params_name}} initialParams) async {
17 | return appNavigator.push(
18 | materialRoute(getIt<{{page_name}}>(param1: initialParams)),
19 | );
20 | }
21 |
22 | AppNavigator get appNavigator;
23 | }
24 |
--------------------------------------------------------------------------------
/templates/page/__brick__/{{{page_absolute_path}}}:
--------------------------------------------------------------------------------
1 | // ignore: unused_import
2 | import 'package:bloc/bloc.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:{{{app_package}}}/core/utils/mvp_extensions.dart';
5 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presentation_model_file_name}}}';
6 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presenter_file_name}}}';
7 |
8 | class {{page_name}} extends StatefulWidget with HasPresenter<{{presenter_name}}> {
9 |
10 | const {{page_name}}({
11 | required this.presenter,
12 | Key? key,
13 | }) : super(key: key);
14 |
15 | @override
16 | final {{presenter_name}} presenter;
17 |
18 | @override
19 | State<{{page_name}}> createState() => _{{page_name}}State();
20 | }
21 |
22 | class _{{page_name}}State extends State<{{page_name}}> with PresenterStateMixin<{{view_model_name}}, {{presenter_name}}, {{page_name}}> {
23 |
24 | @override
25 | Widget build(BuildContext context) => const Scaffold(
26 | body: Center(
27 | child: Text("{{page_name}}\n(NOT IMPLEMENTED YET)"),
28 | ),
29 | );
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/templates/page/__brick__/{{{page_test_absolute_path}}}:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:{{{app_package}}}/dependency_injection/app_component.dart';
3 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{initial_params_file_name}}}';
4 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{navigator_file_name}}}';
5 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{page_file_name}}}';
6 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presentation_model_file_name}}}';
7 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presenter_file_name}}}';
8 |
9 | import '../../../mocks/mocks.dart';
10 | import '../../../test_utils/golden_tests_utils.dart';
11 |
12 | Future main() async {
13 | late {{page_name}} page;
14 | late {{initial_params_name}} initParams;
15 | late {{presentation_model_name}} model;
16 | late {{presenter_name}} presenter;
17 | late {{navigator_name}} navigator;
18 |
19 | void _initMvp() {
20 | initParams = const {{initial_params_name}}();
21 | model = {{presentation_model_name}}.initial(
22 | initParams,
23 | );
24 | navigator = {{navigator_name}}(Mocks.appNavigator);
25 | presenter = {{presenter_name}}(
26 | model,
27 | navigator,
28 | );
29 | page = {{page_name}}(presenter: presenter);
30 | }
31 |
32 | await screenshotTest(
33 | "{{page_name.snakeCase()}}",
34 | setUp: () async {
35 | _initMvp();
36 | },
37 | pageBuilder: () => page,
38 | );
39 |
40 | test("getIt page resolves successfully", () async {
41 | _initMvp();
42 | final page = getIt<{{page_name}}>(param1: initParams);
43 | expect(page.presenter, isNotNull);
44 | expect(page, isNotNull);
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/templates/page/__brick__/{{{presentation_model_absolute_path}}}:
--------------------------------------------------------------------------------
1 | import 'package:{{{app_package}}}/{{{import_path}}}/{{initial_params_file_name}}';
2 |
3 |
4 |
5 | /// Model used by presenter, contains fields that are relevant to presenters and implements ViewModel to expose data to view (page)
6 | class {{presentation_model_name}} implements {{view_model_name}} {
7 | /// Creates the initial state
8 | {{presentation_model_name}}.initial(
9 | // ignore: avoid_unused_constructor_parameters
10 | {{initial_params_name}} initialParams,
11 | );
12 |
13 | /// Used for the copyWith method
14 | {{presentation_model_name}}._();
15 |
16 | {{presentation_model_name}} copyWith() {
17 | return {{presentation_model_name}}._();
18 | }
19 | }
20 |
21 | /// Interface to expose fields used by the view (page).
22 | abstract class {{view_model_name}} {}
23 |
--------------------------------------------------------------------------------
/templates/page/__brick__/{{{presenter_absolute_path}}}:
--------------------------------------------------------------------------------
1 | import 'package:bloc/bloc.dart';
2 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{navigator_file_name}}}';
3 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presentation_model_file_name}}}';
4 |
5 |
6 | class {{presenter_name}} extends Cubit<{{view_model_name}}> {
7 |
8 | {{presenter_name}}(
9 | {{presentation_model_name}} model,
10 | this.navigator,
11 | ) : super(model);
12 |
13 | final {{navigator_name}} navigator;
14 |
15 | // ignore: unused_element
16 | {{presentation_model_name}} get _model => state as {{presentation_model_name}};
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/templates/page/__brick__/{{{presenter_test_absolute_path}}}:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{initial_params_file_name}}}';
3 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presentation_model_file_name}}}';
4 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presenter_file_name}}}';
5 |
6 | import '../mocks/{{{feature}}}_mock_definitions.dart';
7 |
8 | void main() {
9 | late {{presentation_model_name}} model;
10 | late {{presenter_name}} presenter;
11 | late Mock{{navigator_name}} navigator;
12 |
13 | test(
14 | 'sample test',
15 | () {
16 | expect(presenter, isNotNull); // TODO implement this
17 | },
18 | );
19 |
20 | setUp(() {
21 | model = {{presentation_model_name}}.initial(const {{initial_params_name}}());
22 | navigator = Mock{{navigator_name}}();
23 | presenter = {{presenter_name}}(
24 | model,
25 | navigator,
26 | );
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/templates/page/brick.yaml:
--------------------------------------------------------------------------------
1 | name: page
2 | description: Creates Page, Presenter, PresentationModel and relevant tests. Registers the whole structure in getIt dependency graph.
3 |
4 | version: 0.1.0+1
5 |
6 | environment:
7 | mason: ">=0.1.0-dev.26 <0.1.0"
8 |
9 | vars:
10 | page_name:
11 | type: string
12 | description: "Page name"
13 | default: "Circle Details"
14 | prompt: "page's name: "
15 |
16 | feature_name:
17 | type: string
18 | description: "What is the name of the feature?"
19 | default: "circles"
20 | prompt: "Feature name (in snake_case): "
21 |
22 | subdirectory:
23 | type: string
24 | description: "subdirectory name inside the feature (OPTIONAL)"
25 | prompt: "subdirectory name inside the feature (OPTIONAL): "
26 |
27 |
--------------------------------------------------------------------------------
/templates/page/hooks/.gitignore:
--------------------------------------------------------------------------------
1 | .dart_tool
2 | .packages
3 | pubspec.lock
4 |
--------------------------------------------------------------------------------
/templates/page/hooks/pre_gen.dart:
--------------------------------------------------------------------------------
1 | import "package:mason/mason.dart";
2 | import "package:recase/recase.dart";
3 |
4 | Future run(HookContext context) async {
5 | var pageName = (context.vars["page_name"] as String? ?? "").trim().pascalCase;
6 | final featureName = (context.vars["feature_name"] as String? ?? "").trim().snakeCase;
7 | var subdirectory = (context.vars["subdirectory"] as String? ?? "").trim();
8 |
9 | if (pageName.isEmpty) {
10 | throw "Cannot use empty name for page_name";
11 | }
12 | if (featureName.isEmpty) {
13 | throw "Cannot use empty name for feature_name";
14 | }
15 |
16 | final stem = pageName.replaceAll("Page", "");
17 | final featurePath = "features/${featureName}/${subdirectory}" //
18 | .replaceAll(RegExp("/+"), "/")
19 | .replaceAll(RegExp("/\$"), "");
20 | final featureTestPath = "features/${featureName}" //
21 | .replaceAll(RegExp("/+"), "/")
22 | .replaceAll(RegExp("/\$"), "");
23 |
24 | pageName = "${stem}Page";
25 |
26 | final presenterName = "${stem}Presenter";
27 | final presentationModelName = "${stem}PresentationModel";
28 | final initialParamsName = "${stem}InitialParams";
29 | final viewModelName = "${stem}ViewModel";
30 | final navigatorName = "${stem}Navigator";
31 | final routeName = "${stem}Route";
32 |
33 | final pageFileName = "${stem.snakeCase}_page.dart";
34 | final pageTestFileName = "${stem.snakeCase}_page_test.dart";
35 | final presenterFileName = "${stem.snakeCase}_presenter.dart";
36 | final presenterTestFileName = "${stem.snakeCase}_presenter_test.dart";
37 | final presentationModelFileName = "${stem.snakeCase}_presentation_model.dart";
38 | final initialParamsFileName = "${stem.snakeCase}_initial_params.dart";
39 | final navigatorFileName = "${stem.snakeCase}_navigator.dart";
40 |
41 | context.vars = {
42 | ...context.vars,
43 | ...context.vars,
44 | "app_package": "flutter_demo",
45 | "import_path": "${featurePath}",
46 | "stem": "${stem}",
47 | //class names
48 | "page_name": pageName,
49 | "presenter_name": presenterName,
50 | "presentation_model_name": presentationModelName,
51 | "initial_params_name": initialParamsName,
52 | "navigator_name": navigatorName,
53 | "view_model_name": viewModelName,
54 | "route_name": routeName,
55 | //file names
56 | "page_file_name": pageFileName,
57 | "presenter_file_name": presenterFileName,
58 | "initial_params_file_name": initialParamsFileName,
59 | "presentation_model_file_name": presentationModelFileName,
60 | "navigator_file_name": navigatorFileName,
61 | // absolute paths
62 | "page_absolute_path": "../lib/${featurePath}/$pageFileName",
63 | "presenter_absolute_path": "../lib/${featurePath}/$presenterFileName",
64 | "presentation_model_absolute_path": "../lib/${featurePath}/$presentationModelFileName",
65 | "navigator_absolute_path": "../lib/${featurePath}/$navigatorFileName",
66 | "initial_params_absolute_path": "../lib/${featurePath}/$initialParamsFileName",
67 | "page_test_absolute_path": "../test/${featureTestPath}/pages/$pageTestFileName",
68 | "presenter_test_absolute_path": "../test/${featureTestPath}/presenters/$presenterTestFileName",
69 | 'feature': featureName,
70 | };
71 | context.logger.info("Generating page, variables: ${context.vars}");
72 | }
73 |
--------------------------------------------------------------------------------
/templates/page/hooks/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: page_hooks
2 |
3 | environment:
4 | sdk: ">=2.12.0 <3.0.0"
5 |
6 | dependencies:
7 | template_utils:
8 | git:
9 | url: https://github.com/andrzejchm/flutter-app-showcase.git
10 | path: templates/template_utils
11 | ref: main
12 | mason: any
13 | recase: 4.0.0
--------------------------------------------------------------------------------
/templates/repository/README.md:
--------------------------------------------------------------------------------
1 | # repository
2 |
3 | [](https://github.com/felangel/mason)
4 |
5 | A new brick created with the Mason CLI.
6 |
7 | _Generated by [mason][1] 🧱_
8 |
9 | ## Getting Started 🚀
10 |
11 | This is a starting point for a new brick.
12 | A few resources to get you started if this is your first brick template:
13 |
14 | - [Official Mason Documentation][2]
15 | - [Code generation with Mason Blog][3]
16 | - [Very Good Livestream: Felix Angelov Demos Mason][4]
17 |
18 | [1]: https://github.com/felangel/mason
19 | [2]: https://github.com/felangel/mason/tree/master/packages/mason_cli#readme
20 | [3]: https://verygood.ventures/blog/code-generation-with-mason
21 | [4]: https://youtu.be/G4PTjA6tpTU
22 |
--------------------------------------------------------------------------------
/templates/repository/__brick__/{{{implementation_absolute_path}}}:
--------------------------------------------------------------------------------
1 | {{{interface_import}}}
2 |
3 | class {{{implementation_name}}} implements {{{interface_name}}} {
4 | const {{{implementation_name}}}();
5 | }
--------------------------------------------------------------------------------
/templates/repository/__brick__/{{{interface_absolute_path}}}:
--------------------------------------------------------------------------------
1 | abstract class {{{interface_name}}} {
2 |
3 | }
--------------------------------------------------------------------------------
/templates/repository/brick.yaml:
--------------------------------------------------------------------------------
1 | name: repository
2 | description: A new brick created with the Mason CLI.
3 |
4 | # The following defines the version and build number for your brick.
5 | # A version number is three numbers separated by dots, like 1.2.34
6 | # followed by an optional build number (separated by a +).
7 | version: 0.1.0+1
8 |
9 | # The following defines the environment for the current brick.
10 | # It includes the version of mason that the brick requires.
11 | environment:
12 | mason: ">=0.1.0-dev.26 <0.1.0"
13 |
14 | # Variables specify dynamic values that your brick depends on.
15 | # Zero or more variables can be specified for a given brick.
16 | # Each variable has:
17 | # * a type (string, number, boolean, enum, or array)
18 | # * an optional short description
19 | # * an optional default value
20 | # * an optional list of default values (array only)
21 | # * an optional prompt phrase used when asking for the variable
22 | # * a list of values (enums only)
23 | vars:
24 |
25 | interface_name:
26 | type: string
27 | description: UseCase name
28 | default: UsersRepository
29 | prompt: "Repository name: "
30 |
31 | implementation_prefix:
32 | type: string
33 | description: UseCase name
34 | default: RestApi
35 | prompt: "Repository implementation class prefix: "
36 |
37 | feature_name:
38 | type: string
39 | description: "What is the name of the feature? Leave blank if it should be in core package"
40 | prompt: "Feature name (in snake_case), leave blank if it should be in 'core' package: "
41 |
--------------------------------------------------------------------------------
/templates/repository/hooks/.gitignore:
--------------------------------------------------------------------------------
1 | .dart_tool
2 | .packages
3 | pubspec.lock
4 |
--------------------------------------------------------------------------------
/templates/repository/hooks/post_gen.dart:
--------------------------------------------------------------------------------
1 | import 'package:mason/mason.dart';
2 | import 'package:recase/recase.dart';
3 | import 'package:template_utils/template_utils.dart';
4 |
5 | Future run(HookContext context) async {
6 | final appPackage = context.vars["app_package"] as String;
7 | final feature = context.vars["feature"] as String;
8 | final interfaceName = context.vars["interface_name"] as String;
9 | final implementationName = context.vars["implementation_name"] as String;
10 | final interfaceFileName = context.vars["interface_file_name"] as String;
11 | final implementationFileName = context.vars["implementation_file_name"] as String;
12 | final implementationImport = context.vars["implementation_import"] as String;
13 | final interfaceImport = context.vars["interface_import"] as String;
14 |
15 | await _replaceInMockDefinitions(
16 | context: context,
17 | interfaceName: interfaceName,
18 | interfaceFileName: interfaceFileName,
19 | interfaceImport: interfaceImport,
20 | feature: feature,
21 | );
22 |
23 | await _replaceInMocks(
24 | context: context,
25 | interfaceName: interfaceName,
26 | feature: feature,
27 | );
28 |
29 | await _replaceInAppComponent(
30 | appPackage: appPackage,
31 | context: context,
32 | interfaceName: interfaceName,
33 | implementationName: implementationName,
34 | interfaceFileName: interfaceFileName,
35 | implementationFileName: implementationFileName,
36 | implementationImport: implementationImport,
37 | interfaceImport: interfaceImport,
38 | feature: feature,
39 | );
40 | }
41 |
42 | Future _replaceInAppComponent({
43 | required HookContext context,
44 | required String appPackage,
45 | required String interfaceName,
46 | required String implementationName,
47 | required String interfaceFileName,
48 | required String implementationFileName,
49 | required String implementationImport,
50 | required String interfaceImport,
51 | required String feature,
52 | }) async {
53 | await ensureFeatureComponentFile(appPackage: appPackage, feature: feature);
54 | await replaceAllInFile(
55 | filePath: featureComponentFilePath(feature),
56 | from: "//DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG",
57 | to: """
58 | ..registerFactory<$interfaceName>(
59 | () => const $implementationName(),
60 | )
61 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG
62 | """,
63 | );
64 | await replaceAllInFile(
65 | filePath: featureComponentFilePath(feature),
66 | from: "//DO-NOT-REMOVE APP_COMPONENT_IMPORTS",
67 | to: """
68 | $implementationImport
69 | $interfaceImport
70 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS
71 | """,
72 | );
73 | }
74 |
75 | Future _replaceInMockDefinitions({
76 | required HookContext context,
77 | required String interfaceName,
78 | required String interfaceFileName,
79 | required String interfaceImport,
80 | required String feature,
81 | }) async {
82 | final mockDefinition = (String name) => "class Mock$name extends Mock implements $name {}";
83 | await ensureMockDefinitionsFile(feature);
84 | await replaceAllInFile(
85 | filePath: mockDefinitionsFilePath(feature),
86 | from: "//DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS",
87 | to: """
88 | $interfaceImport
89 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS
90 | """,
91 | );
92 |
93 | await replaceAllInFile(
94 | filePath: mockDefinitionsFilePath(feature),
95 | from: "//DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION",
96 | to: """
97 | ${mockDefinition(interfaceName)}
98 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION
99 | """,
100 | );
101 | }
102 |
103 | Future _replaceInMocks({
104 | required HookContext context,
105 | required String interfaceName,
106 | required String feature,
107 | }) async {
108 | final mockStaticField = (String name) => "static late Mock$name ${name.camelCase};";
109 | final mockInit = (String name) => "${name.camelCase} = Mock$name();";
110 | final registerFallbackValue = (String name) => "registerFallbackValue(Mock$name());";
111 |
112 | await ensureMocksFile(feature);
113 | await replaceAllInFile(
114 | filePath: mocksFilePath(feature),
115 | from: "//DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD",
116 | to: """
117 | ${mockStaticField(interfaceName)}
118 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD
119 | """,
120 | );
121 | await replaceAllInFile(
122 | filePath: mocksFilePath(feature),
123 | from: "//DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS",
124 | to: """
125 | ${mockInit(interfaceName)}
126 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS
127 | """,
128 | );
129 | await replaceAllInFile(
130 | filePath: mocksFilePath(feature),
131 | from: "//DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE",
132 | to: """
133 | ${registerFallbackValue(interfaceName)}
134 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE
135 | """,
136 | );
137 | }
138 |
--------------------------------------------------------------------------------
/templates/repository/hooks/pre_gen.dart:
--------------------------------------------------------------------------------
1 | import "package:mason/mason.dart";
2 | import "package:recase/recase.dart";
3 |
4 | Future run(HookContext context) async {
5 | var interfaceName = (context.vars["interface_name"] as String? ?? "").trim().pascalCase;
6 | var implementationPrefix = (context.vars["implementation_prefix"] as String? ?? "").trim().pascalCase;
7 | final featureName = (context.vars["feature_name"] as String? ?? "").trim().snakeCase;
8 |
9 | if (interfaceName.isEmpty) {
10 | throw "Cannot use empty name for repository";
11 | }
12 | if (implementationPrefix.isEmpty) {
13 | throw "Cannot use empty prefix for repository implementation";
14 | }
15 |
16 | final stem = interfaceName.replaceAll("Repository", "");
17 | interfaceName = "${stem}Repository";
18 | final implementationName = "${implementationPrefix}${stem}Repository";
19 |
20 | final interfaceFileName = "${stem.snakeCase}_repository.dart";
21 | final implementationFileName = "${implementationPrefix.snakeCase}_${interfaceFileName}";
22 |
23 | final featurePath = featureName.isEmpty ? "core" : "features/${featureName}";
24 |
25 | var appPackage = "flutter_demo";
26 | context.vars = {
27 | ...context.vars,
28 | "app_package": appPackage,
29 | "stem": "${stem}",
30 | "interface_name": interfaceName,
31 | "implementation_name": implementationName,
32 | "interface_file_name": interfaceFileName,
33 | "implementation_file_name": implementationFileName,
34 | "interface_absolute_path": "../lib/$featurePath/domain/repositories/$interfaceFileName",
35 | "implementation_absolute_path": "../lib/$featurePath/data/$implementationFileName",
36 | "interface_import": "import 'package:$appPackage/$featurePath/domain/repositories/$interfaceFileName';",
37 | "implementation_import": "import 'package:$appPackage/$featurePath/data/$implementationFileName';",
38 | "feature": featureName,
39 | };
40 | context.logger.info("Generating useCase, variables: ${context.vars}");
41 | }
42 |
--------------------------------------------------------------------------------
/templates/repository/hooks/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: use_case_hooks
2 |
3 | environment:
4 | sdk: ">=2.12.0 <3.0.0"
5 |
6 | dependencies:
7 | template_utils:
8 | git:
9 | url: https://github.com/andrzejchm/flutter-app-showcase.git
10 | path: templates/template_utils
11 | ref: main
12 | mason: any
13 | recase: 4.0.0
--------------------------------------------------------------------------------
/templates/template_utils/.gitignore:
--------------------------------------------------------------------------------
1 | # Files and directories created by pub.
2 | .dart_tool/
3 | .packages
4 |
5 | # Conventional directory for build output.
6 | build/
7 |
--------------------------------------------------------------------------------
/templates/template_utils/README.md:
--------------------------------------------------------------------------------
1 | A sample command-line application with an entrypoint in `bin/`, library code
2 | in `lib/`, and example unit test in `test/`.
3 |
--------------------------------------------------------------------------------
/templates/template_utils/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the static analysis results for your project (errors,
2 | # warnings, and lints).
3 | #
4 | # This enables the 'recommended' set of lints from `package:lints`.
5 | # This set helps identify many issues that may lead to problems when running
6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic
7 | # style and format.
8 | #
9 | # If you want a smaller set of lints you can change this to specify
10 | # 'package:lints/core.yaml'. These are just the most critical lints
11 | # (the recommended set includes the core lints).
12 | # The core lints are also what is used by pub.dev for scoring packages.
13 |
14 | include: package:lints/recommended.yaml
15 |
16 | # Uncomment the following section to specify additional rules.
17 |
18 | # linter:
19 | # rules:
20 | # - camel_case_types
21 |
22 | # analyzer:
23 | # exclude:
24 | # - path/to/excluded/files/**
25 |
26 | # For more information about the core and recommended set of lints, see
27 | # https://dart.dev/go/core-lints
28 |
29 | # For additional information about configuring this file, see
30 | # https://dart.dev/guides/language/analysis-options
31 |
--------------------------------------------------------------------------------
/templates/template_utils/lib/feature_templates.dart:
--------------------------------------------------------------------------------
1 | import 'package:recase/recase.dart';
2 |
3 | String featureMockDefinitionsTemplate = """
4 | import 'package:mocktail/mocktail.dart';
5 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS
6 |
7 | // MVP
8 |
9 | //DO-NOT-REMOVE MVP_MOCK_DEFINITION
10 |
11 | // USE CASES
12 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION
13 |
14 | // REPOSITORIES
15 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION
16 |
17 | // STORES
18 | //DO-NOT-REMOVE STORES_MOCK_DEFINITION
19 |
20 | """;
21 |
22 | String featureMocksTemplate(String feature) => """
23 | import 'package:mocktail/mocktail.dart';
24 |
25 | import '${feature.snakeCase}_mock_definitions.dart';
26 | //DO-NOT-REMOVE IMPORTS_MOCKS
27 |
28 | class ${feature.pascalCase}Mocks {
29 |
30 | // MVP
31 |
32 | //DO-NOT-REMOVE MVP_MOCKS_STATIC_FIELD
33 |
34 | // USE CASES
35 |
36 |
37 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD
38 |
39 | // REPOSITORIES
40 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD
41 |
42 | // STORES
43 |
44 | //DO-NOT-REMOVE STORES_MOCKS_STATIC_FIELD
45 |
46 |
47 | static void init() {
48 | _initMocks();
49 | _initFallbacks();
50 | }
51 |
52 | static void _initMocks() {
53 | //DO-NOT-REMOVE FEATURES_MOCKS
54 | // MVP
55 | //DO-NOT-REMOVE MVP_INIT_MOCKS
56 |
57 | // USE CASES
58 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS
59 |
60 | // REPOSITORIES
61 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS
62 |
63 | // STORES
64 | //DO-NOT-REMOVE STORES_INIT_MOCKS
65 |
66 | }
67 |
68 | static void _initFallbacks() {
69 | //DO-NOT-REMOVE FEATURES_FALLBACKS
70 | // MVP
71 | //DO-NOT-REMOVE MVP_MOCK_FALLBACK_VALUE
72 |
73 | // USE CASES
74 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE
75 |
76 | // REPOSITORIES
77 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE
78 |
79 | // STORES
80 | //DO-NOT-REMOVE STORES_MOCK_FALLBACK_VALUE
81 |
82 | }
83 | }
84 | """;
85 |
86 | String featureComponentTemplate(String appPackage) => """
87 | import 'package:$appPackage/dependency_injection/app_component.dart';
88 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS
89 |
90 | /// registers all the dependencies in dependency graph in get_it package
91 | void configureDependencies() {
92 | _configureGeneralDependencies();
93 | _configureRepositories();
94 | _configureStores();
95 | _configureUseCases();
96 | _configureMvp();
97 | }
98 |
99 | //ignore: long-method
100 | void _configureGeneralDependencies() {
101 | // ignore: unnecessary_statements
102 | getIt
103 | //DO-NOT-REMOVE GENERAL_DEPS_GET_IT_CONFIG
104 | ;
105 | }
106 |
107 | //ignore: long-method
108 | void _configureRepositories() {
109 | // ignore: unnecessary_statements
110 | getIt
111 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG
112 | ;
113 | }
114 |
115 | //ignore: long-method
116 | void _configureStores() {
117 | // ignore: unnecessary_statements
118 | getIt
119 | //DO-NOT-REMOVE STORES_GET_IT_CONFIG
120 | ;
121 | }
122 |
123 | //ignore: long-method
124 | void _configureUseCases() {
125 | // ignore: unnecessary_statements
126 | getIt
127 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG
128 | ;
129 | }
130 |
131 | //ignore: long-method
132 | void _configureMvp() {
133 | // ignore: unnecessary_statements
134 | getIt
135 | //DO-NOT-REMOVE MVP_GET_IT_CONFIG
136 | ;
137 | }
138 | """;
139 |
140 | String featurePageTestConfigTemplate = """
141 | import 'dart:async';
142 |
143 | import '../../../test_utils/test_utils.dart';
144 |
145 | Future testExecutable(FutureOr Function() testMain) => preparePageTests(testMain);
146 | """;
147 |
--------------------------------------------------------------------------------
/templates/template_utils/lib/file_utils.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:io';
3 |
4 | import 'package:mason/mason.dart';
5 | import 'package:recase/recase.dart';
6 | import 'package:template_utils/feature_templates.dart';
7 |
8 | String featureComponentFilePath(String? feature) => ((feature?.isEmpty ?? true) || feature == 'core') //
9 | ? '../lib/dependency_injection/app_component.dart'
10 | : '../lib/features/$feature/dependency_injection/feature_component.dart';
11 |
12 | String mockDefinitionsFilePath(String? feature) => ((feature?.isEmpty ?? true) || feature == 'core') //
13 | ? '../test/mocks/mock_definitions.dart'
14 | : '../test/features/$feature/mocks/${feature}_mock_definitions.dart';
15 |
16 | String mocksFilePath(String? feature) => ((feature?.isEmpty ?? true) || feature == 'core') //
17 | ? '../test/mocks/mocks.dart'
18 | : '../test/features/$feature/mocks/${feature}_mocks.dart';
19 |
20 | String pagesTestConfigPath(String feature) => '../test/features/$feature/pages/flutter_test_config.dart';
21 |
22 | /// makes sure the feature-specific getIt registration index file is created,
23 | /// if its not, creates one and registers in master `app_component.dart` file
24 | Future ensureFeatureComponentFile({
25 | required String appPackage,
26 | required String? feature,
27 | }) async {
28 | var featurePath = featureComponentFilePath(feature);
29 | var filePackage = featurePath.replaceAll("../lib/features/", "");
30 | final featureFile = File(featurePath);
31 | final coreFile = File(featureComponentFilePath(null));
32 | if (!await featureFile.exists()) {
33 | await featureFile.create(recursive: true);
34 | await writeToFile(filePath: featureFile.path, text: featureComponentTemplate(appPackage));
35 | await replaceAllInFile(
36 | filePath: coreFile.path,
37 | from: "//DO-NOT-REMOVE APP_COMPONENT_IMPORTS",
38 | to: """
39 | import 'package:$appPackage/features/$filePackage' as $feature;
40 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS
41 | """,
42 | );
43 | await replaceAllInFile(
44 | filePath: coreFile.path,
45 | from: "//DO-NOT-REMOVE FEATURE_COMPONENT_INIT",
46 | to: """
47 | $feature.configureDependencies();
48 | //DO-NOT-REMOVE FEATURE_COMPONENT_INIT
49 | """,
50 | );
51 | }
52 | }
53 |
54 | /// makes sure the feature-specific mock definitions file is created, if its not, creates one
55 | Future ensureMockDefinitionsFile(
56 | String? feature, {
57 | HookContext? context,
58 | }) async {
59 | var featurePath = mockDefinitionsFilePath(feature);
60 | final featureFile = File(featurePath).absolute;
61 | final coreFile = File(mockDefinitionsFilePath(null)).absolute;
62 | context?.logger.write("feature mocks file: ${featureFile.path}");
63 | context?.logger.write("core file: ${coreFile.path}");
64 | if (!await featureFile.exists()) {
65 | await featureFile.create(recursive: true);
66 | await writeToFile(filePath: featureFile.path, text: featureMockDefinitionsTemplate);
67 | }
68 | }
69 |
70 | /// makes sure the feature-specific mocks file is created,
71 | /// if its not, creates one and registers in master `mocks.dart` file
72 | Future ensureMocksFile(
73 | String? feature, {
74 | HookContext? context,
75 | }) async {
76 | var featurePath = mocksFilePath(feature);
77 | var filePackage = featurePath.replaceAll("../test/", "../");
78 | final featureFile = File(featurePath);
79 | final coreFile = File(mocksFilePath(null));
80 | await _ensurePageTestConfigFile(feature);
81 | if (!await featureFile.exists()) {
82 | await featureFile.create(recursive: true);
83 | await writeToFile(filePath: featureFile.path, text: featureMocksTemplate(feature!));
84 | await replaceAllInFile(
85 | filePath: coreFile.path,
86 | from: "//DO-NOT-REMOVE IMPORTS_MOCKS",
87 | to: """
88 | import '$filePackage';
89 | //DO-NOT-REMOVE IMPORTS_MOCKS
90 | """,
91 | );
92 | await replaceAllInFile(
93 | filePath: coreFile.path,
94 | from: "//DO-NOT-REMOVE FEATURE_MOCKS_INIT",
95 | to: """
96 | ${feature.pascalCase}Mocks.init();
97 | //DO-NOT-REMOVE FEATURE_MOCKS_INIT
98 | """,
99 | );
100 | }
101 | }
102 |
103 | Future _ensurePageTestConfigFile(String? feature) async {
104 | if (feature == null || feature.isEmpty) {
105 | return;
106 | }
107 | final testConfigFile = File(pagesTestConfigPath(feature)).absolute;
108 |
109 | if (!await testConfigFile.exists()) {
110 | await testConfigFile.create(recursive: true);
111 | await writeToFile(filePath: testConfigFile.path, text: featurePageTestConfigTemplate);
112 | }
113 | }
114 |
115 | Future replaceAllInFile({
116 | required String filePath,
117 | required String from,
118 | required String to,
119 | }) async {
120 | final tmpFilePath = "${filePath}_write_.tmp";
121 | final tmpFile = File(tmpFilePath);
122 | bool contains = false;
123 | try {
124 | final readStream = readFileLines(filePath);
125 | final writeSink = tmpFile.openWrite();
126 |
127 | await for (var line in readStream) {
128 | if (line.contains(from)) {
129 | contains = true;
130 | }
131 | writeSink.writeln(line.replaceAll(from, to));
132 | }
133 | if (!contains) {
134 | throw "Target file ($filePath) does not contain '$from' text inside";
135 | }
136 | await writeSink.close();
137 | } catch (ex) {
138 | tmpFile.deleteSync();
139 | rethrow;
140 | }
141 |
142 | await tmpFile.rename(filePath);
143 | }
144 |
145 | Future writeToFile({
146 | required String filePath,
147 | required String text,
148 | }) async {
149 | final tmpFilePath = "${filePath}_write_.tmp";
150 | final tmpFile = File(tmpFilePath);
151 |
152 | try {
153 | await tmpFile.writeAsString(text);
154 | } catch (ex) {
155 | tmpFile.deleteSync();
156 | rethrow;
157 | }
158 |
159 | await tmpFile.rename(filePath);
160 | }
161 |
162 | Stream readFileLines(String path) =>
163 | File(path).openRead().transform(utf8.decoder).transform(const LineSplitter());
164 |
165 | void main() {
166 | ensureMockDefinitionsFile("sample_feature");
167 | }
168 |
--------------------------------------------------------------------------------
/templates/template_utils/lib/template_utils.dart:
--------------------------------------------------------------------------------
1 | library template_utils;
2 |
3 | export 'file_utils.dart';
4 |
--------------------------------------------------------------------------------
/templates/template_utils/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: template_utils
2 | description: A sample command-line application.
3 | version: 1.0.0
4 | # homepage: https://www.example.com
5 |
6 | environment:
7 | sdk: '>=2.17.5 <3.0.0'
8 |
9 | dependencies:
10 | mason: any
11 | recase: 4.0.0
12 |
13 | dev_dependencies:
14 | lints: 2.0.0
15 | test: 1.21.4
16 |
17 |
--------------------------------------------------------------------------------
/templates/use_case/README.md:
--------------------------------------------------------------------------------
1 | # use_case
2 |
3 | [](https://github.com/felangel/mason)
4 |
5 | Creates the UseCase along with the associated failure
6 |
7 | creates following files:
8 |
9 | ```
10 | - lib/features/$FEATURE_NAME$/domain/use_cases/$NAME$_use_case.dart
11 | - lib/features/$FEATURE_NAME$/domain/failures/$NAME$_failure.dart
12 |
13 | - test/domain/$NAME$_use_case_test.dart
14 | ```
15 |
16 | also modifies following files
17 |
18 | - `app_component.dart` - adds getIt registration for the use case
19 | - `test/mock_definitions.dart` - adds mock definitions for the use case
20 | - `test/mocks.dart` - creates the mocks as static fields for use in tests
--------------------------------------------------------------------------------
/templates/use_case/__brick__/{{{failure_absolute_path}}}:
--------------------------------------------------------------------------------
1 | import 'package:{{{app_package}}}/core/domain/model/displayable_failure.dart';
2 |
3 | class {{failure_name}} implements HasDisplayableFailure {
4 | // ignore: avoid_field_initializers_in_const_classes
5 | const {{failure_name}}.unknown([this.cause]) : type = {{failure_name}}Type.Unknown;
6 |
7 | final {{failure_name}}Type type;
8 | final Object? cause;
9 |
10 | @override
11 | DisplayableFailure displayableFailure() {
12 | switch (type) {
13 | case {{failure_name}}Type.Unknown:
14 | return DisplayableFailure.commonError();
15 | }
16 | }
17 |
18 | @override
19 | String toString() => '{{failure_name}}{type: $type, cause: $cause}';
20 | }
21 |
22 | enum {{failure_name}}Type {
23 | Unknown,
24 | }
25 |
--------------------------------------------------------------------------------
/templates/use_case/__brick__/{{{use_case_absolute_path}}}:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:{{{app_package}}}/core/utils/either_extensions.dart';
3 | import 'package:{{{app_package}}}/{{{import_path}}}/domain/model/{{{failure_file_name}}}';
4 |
5 | class {{use_case_name}} {
6 | const {{use_case_name}}();
7 |
8 | Future> execute() async {
9 | return failure(const {{failure_name}}.unknown());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/templates/use_case/__brick__/{{{use_case_test_absolute_path}}}:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:{{{app_package}}}/core/utils/either_extensions.dart';
3 | import 'package:{{{app_package}}}/dependency_injection/app_component.dart';
4 | import 'package:{{{app_package}}}/{{{import_path}}}/domain/use_cases/{{{use_case_file_name}}}';
5 |
6 | void main() {
7 | late {{use_case_name}} useCase;
8 |
9 | setUp(() {
10 | useCase = const {{use_case_name}}();
11 | });
12 |
13 | test(
14 | 'use case executes normally',
15 | () async {
16 | // GIVEN
17 |
18 | // WHEN
19 | final result = await useCase.execute();
20 |
21 | // THEN
22 | expect(result.isSuccess, true);
23 | },
24 | );
25 |
26 |
27 | test("getIt resolves successfully", () async {
28 | final useCase = getIt<{{use_case_name}}>();
29 | expect(useCase, isNotNull);
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/templates/use_case/brick.yaml:
--------------------------------------------------------------------------------
1 | name: use_case
2 | description: Create use case and failure classes
3 |
4 | # The following defines the version and build number for your brick.
5 | # A version number is three numbers separated by dots, like 1.2.34
6 | # followed by an optional build number (separated by a +).
7 | version: 0.1.0+1
8 |
9 | # The following defines the environment for the current brick.
10 | # It includes the version of mason that the brick requires.
11 | environment:
12 | mason: ">=0.1.0-dev.26 <0.1.0"
13 |
14 | # Variables specify dynamic values that your brick depends on.
15 | # Zero or more variables can be specified for a given brick.
16 | # Each variable has:
17 | # * a type (string, number, boolean, enum, or array)
18 | # * an optional short description
19 | # * an optional default value
20 | # * an optional list of default values (array only)
21 | # * an optional prompt phrase used when asking for the variable
22 | # * a list of values (enums only)
23 | vars:
24 | use_case_name:
25 | type: string
26 | description: UseCase name
27 | default: LogInUseCase
28 | prompt: "UseCase name:"
29 |
30 | feature_name:
31 | type: string
32 | description: "What is the name of the feature? Leave blank if it should be in core package"
33 | prompt: "Feature name (in snake_case), leave blank if it should be in 'core' package"
34 |
--------------------------------------------------------------------------------
/templates/use_case/hooks/.gitignore:
--------------------------------------------------------------------------------
1 | .dart_tool
2 | .packages
3 | pubspec.lock
4 |
--------------------------------------------------------------------------------
/templates/use_case/hooks/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the static analysis results for your project (errors,
2 | # warnings, and lints).
3 | #
4 | # This enables the 'recommended' set of lints from `package:lints`.
5 | # This set helps identify many issues that may lead to problems when running
6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic
7 | # style and format.
8 | #
9 | # If you want a smaller set of lints you can change this to specify
10 | # 'package:lints/core.yaml'. These are just the most critical lints
11 | # (the recommended set includes the core lints).
12 | # The core lints are also what is used by pub.dev for scoring packages.
13 |
14 | include: package:lints/recommended.yaml
15 |
16 | # Uncomment the following section to specify additional rules.
17 |
18 | # linter:
19 | # rules:
20 | # - camel_case_types
21 |
22 | # analyzer:
23 | # exclude:
24 | # - path/to/excluded/files/**
25 |
26 | # For more information about the core and recommended set of lints, see
27 | # https://dart.dev/go/core-lints
28 |
29 | # For additional information about configuring this file, see
30 | # https://dart.dev/guides/language/analysis-options
31 |
--------------------------------------------------------------------------------
/templates/use_case/hooks/post_gen.dart:
--------------------------------------------------------------------------------
1 | import 'package:mason/mason.dart';
2 | import 'package:recase/recase.dart';
3 | import 'package:template_utils/template_utils.dart';
4 |
5 | Future run(HookContext context) async {
6 | try {
7 | final useCaseName = context.vars["use_case_name"] as String;
8 | final failureName = context.vars["failure_name"] as String;
9 | final importPath = context.vars["import_path"] as String;
10 | final useCaseFileName = context.vars["use_case_file_name"] as String;
11 | final failureFileName = context.vars["failure_file_name"] as String;
12 | final appPackage = context.vars["app_package"] as String;
13 | final feature = context.vars["feature"] as String;
14 |
15 | context.logger.info("Modifying mock definitions...");
16 | await _replaceInMockDefinitions(
17 | context: context,
18 | appPackage: appPackage,
19 | importPath: importPath,
20 | useCaseName: useCaseName,
21 | failureName: failureName,
22 | useCaseFileName: useCaseFileName,
23 | failureFileName: failureFileName,
24 | feature: feature,
25 | );
26 |
27 | context.logger.info("Modifying mocks...");
28 | await _replaceInMocks(
29 | context: context,
30 | useCaseName: useCaseName,
31 | failureName: failureName,
32 | feature: feature,
33 | );
34 |
35 | context.logger.info("Modifying feature component...");
36 | await _replaceInAppComponent(
37 | context: context,
38 | useCaseName: useCaseName,
39 | importPath: importPath,
40 | useCaseFileName: useCaseFileName,
41 | failureFileName: failureFileName,
42 | appPackage: appPackage,
43 | feature: feature,
44 | );
45 | } catch (ex, stack) {
46 | context.logger.err("$ex\n$stack");
47 | }
48 | }
49 |
50 | Future _replaceInAppComponent({
51 | required HookContext context,
52 | required String useCaseName,
53 | required String importPath,
54 | required String useCaseFileName,
55 | required String failureFileName,
56 | required String appPackage,
57 | required String feature,
58 | }) async {
59 | await ensureFeatureComponentFile(appPackage: appPackage, feature: feature);
60 | await replaceAllInFile(
61 | filePath: featureComponentFilePath(feature),
62 | from: "//DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG",
63 | to: """
64 | ..registerFactory<$useCaseName>(
65 | () => const $useCaseName(),
66 | )
67 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG
68 | """,
69 | );
70 | await replaceAllInFile(
71 | filePath: featureComponentFilePath(feature),
72 | from: "//DO-NOT-REMOVE APP_COMPONENT_IMPORTS",
73 | to: """
74 | import 'package:$appPackage/$importPath/domain/use_cases/$useCaseFileName';
75 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS
76 | """,
77 | );
78 | }
79 |
80 | Future _replaceInMockDefinitions({
81 | required HookContext context,
82 | required String appPackage,
83 | required String importPath,
84 | required String useCaseName,
85 | required String useCaseFileName,
86 | required String failureName,
87 | required String failureFileName,
88 | required String feature,
89 | }) async {
90 | final mockDefinition = (String name) => "class Mock$name extends Mock implements $name {}";
91 | await ensureMockDefinitionsFile(feature, context: context);
92 | await replaceAllInFile(
93 | filePath: mockDefinitionsFilePath(feature),
94 | from: "//DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS",
95 | to: """
96 | import 'package:$appPackage/$importPath/domain/use_cases/$useCaseFileName';
97 | import 'package:$appPackage/$importPath/domain/model/$failureFileName';
98 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS
99 | """,
100 | );
101 |
102 | await replaceAllInFile(
103 | filePath: mockDefinitionsFilePath(feature),
104 | from: "//DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION",
105 | to: """
106 | ${mockDefinition(failureName)}
107 | ${mockDefinition(useCaseName)}
108 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION
109 | """,
110 | );
111 | }
112 |
113 | Future _replaceInMocks({
114 | required HookContext context,
115 | required String useCaseName,
116 | required String failureName,
117 | required String feature,
118 | }) async {
119 | final mockStaticField = (String name) => "static late Mock$name ${name.camelCase};";
120 | final mockInit = (String name) => "${name.camelCase} = Mock$name();";
121 | final registerFallbackValue = (String name) => "registerFallbackValue(Mock$name());";
122 |
123 | await ensureMocksFile(feature);
124 |
125 | await replaceAllInFile(
126 | filePath: mocksFilePath(feature),
127 | from: "//DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD",
128 | to: """
129 | ${mockStaticField(failureName)}
130 | ${mockStaticField(useCaseName)}
131 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD
132 | """,
133 | );
134 | await replaceAllInFile(
135 | filePath: mocksFilePath(feature),
136 | from: "//DO-NOT-REMOVE USE_CASE_INIT_MOCKS",
137 | to: """
138 | ${mockInit(failureName)}
139 | ${mockInit(useCaseName)}
140 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS
141 | """,
142 | );
143 | await replaceAllInFile(
144 | filePath: mocksFilePath(feature),
145 | from: "//DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE",
146 | to: """
147 | ${registerFallbackValue(failureName)}
148 | ${registerFallbackValue(useCaseName)}
149 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE
150 | """,
151 | );
152 | }
153 |
--------------------------------------------------------------------------------
/templates/use_case/hooks/pre_gen.dart:
--------------------------------------------------------------------------------
1 | import "package:mason/mason.dart";
2 | import "package:recase/recase.dart";
3 |
4 | Future run(HookContext context) async {
5 | var useCaseName = (context.vars["use_case_name"] as String? ?? "").trim().pascalCase;
6 | final featureName = (context.vars["feature_name"] as String? ?? "").trim().snakeCase;
7 |
8 | if (useCaseName.isEmpty) {
9 | throw "Cannot use empty name for usecase";
10 | }
11 |
12 | final stem = useCaseName.replaceAll("UseCase", "");
13 | useCaseName = "${stem}UseCase";
14 |
15 | final failureName = "${stem}Failure";
16 | final useCaseFileName = "${stem.snakeCase}_use_case.dart";
17 | final failureFileName = "${stem.snakeCase}_failure.dart";
18 | final featurePath = featureName.isEmpty ? "core" : "features/${featureName}";
19 |
20 | context.vars = {
21 | ...context.vars,
22 | "app_package": "flutter_demo",
23 | "import_path": "${featurePath}",
24 | "stem": "${stem}",
25 | "failure_name": failureName,
26 | "use_case_name": useCaseName,
27 | "use_case_file_name": useCaseFileName,
28 | "failure_file_name": failureFileName,
29 | "use_case_absolute_path": "../lib/${featurePath}/domain/use_cases/$useCaseFileName",
30 | "failure_absolute_path": "../lib/${featurePath}/domain/model/$failureFileName",
31 | "use_case_test_absolute_path": "../test/${featurePath}/domain/${stem.snakeCase}_use_case_test.dart",
32 | 'feature': featureName,
33 | };
34 | context.logger.info("Generating useCase, variables: ${context.vars}");
35 | }
36 |
--------------------------------------------------------------------------------
/templates/use_case/hooks/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: use_case_hooks
2 |
3 | environment:
4 | sdk: ">=2.12.0 <3.0.0"
5 |
6 | dependencies:
7 | template_utils:
8 | git:
9 | url: https://github.com/andrzejchm/flutter-app-showcase.git
10 | path: templates/template_utils
11 | ref: main
12 | mason: any
13 | recase: 4.0.0
--------------------------------------------------------------------------------
/test/features/app_init/domain/app_init_use_case_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/core/domain/use_cases/app_init_use_case.dart';
2 | import 'package:flutter_demo/dependency_injection/app_component.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 |
5 | void main() {
6 | late AppInitUseCase useCase;
7 |
8 | setUp(() {
9 | useCase = const AppInitUseCase();
10 | });
11 |
12 | test(
13 | 'use case executes normally',
14 | () async {
15 | // GIVEN
16 |
17 | // WHEN
18 | final result = await useCase.execute();
19 |
20 | // THEN
21 | expect(result.isRight(), true);
22 | },
23 | );
24 |
25 | test(
26 | "getIt resolves successfully",
27 | () async {
28 | final useCase = getIt();
29 | expect(useCase, isNotNull);
30 | },
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/test/features/app_init/mocks/app_init_mock_definitions.dart:
--------------------------------------------------------------------------------
1 | import 'package:bloc_test/bloc_test.dart';
2 | import 'package:flutter_demo/core/domain/model/app_init_failure.dart';
3 | import 'package:flutter_demo/core/domain/use_cases/app_init_use_case.dart';
4 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart';
5 | import 'package:flutter_demo/features/app_init/app_init_navigator.dart';
6 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart';
7 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart';
8 | import 'package:mocktail/mocktail.dart';
9 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS
10 |
11 | // MVP
12 |
13 | class MockAppInitPresenter extends MockCubit implements AppInitPresenter {}
14 |
15 | class MockAppInitPresentationModel extends Mock implements AppInitPresentationModel {}
16 |
17 | class MockAppInitInitialParams extends Mock implements AppInitInitialParams {}
18 |
19 | class MockAppInitNavigator extends Mock implements AppInitNavigator {}
20 |
21 | //DO-NOT-REMOVE MVP_MOCK_DEFINITION
22 |
23 | // USE CASES
24 | class MockAppInitFailure extends Mock implements AppInitFailure {}
25 |
26 | class MockAppInitUseCase extends Mock implements AppInitUseCase {}
27 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION
28 |
29 | // REPOSITORIES
30 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION
31 |
32 | // STORES
33 | //DO-NOT-REMOVE STORES_MOCK_DEFINITION
34 |
--------------------------------------------------------------------------------
/test/features/app_init/mocks/app_init_mocks.dart:
--------------------------------------------------------------------------------
1 | import 'package:mocktail/mocktail.dart';
2 |
3 | import 'app_init_mock_definitions.dart';
4 | //DO-NOT-REMOVE IMPORTS_MOCKS
5 |
6 | class AppInitMocks {
7 | // MVP
8 |
9 | static late MockAppInitPresenter appInitPresenter;
10 | static late MockAppInitPresentationModel appInitPresentationModel;
11 | static late MockAppInitInitialParams appInitInitialParams;
12 | static late MockAppInitNavigator appInitNavigator;
13 |
14 | //DO-NOT-REMOVE MVP_MOCKS_STATIC_FIELD
15 |
16 | // USE CASES
17 | static late MockAppInitFailure appInitFailure;
18 | static late MockAppInitUseCase appInitUseCase;
19 |
20 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD
21 |
22 | // REPOSITORIES
23 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD
24 |
25 | // STORES
26 |
27 | //DO-NOT-REMOVE STORES_MOCKS_STATIC_FIELD
28 |
29 | static void init() {
30 | _initMocks();
31 | _initFallbacks();
32 | }
33 |
34 | static void _initMocks() {
35 | //DO-NOT-REMOVE FEATURES_MOCKS
36 | // MVP
37 | appInitPresenter = MockAppInitPresenter();
38 | appInitPresentationModel = MockAppInitPresentationModel();
39 | appInitInitialParams = MockAppInitInitialParams();
40 | appInitNavigator = MockAppInitNavigator();
41 | //DO-NOT-REMOVE MVP_INIT_MOCKS
42 |
43 | // USE CASES
44 | appInitFailure = MockAppInitFailure();
45 | appInitUseCase = MockAppInitUseCase();
46 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS
47 |
48 | // REPOSITORIES
49 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS
50 |
51 | // STORES
52 | //DO-NOT-REMOVE STORES_INIT_MOCKS
53 | }
54 |
55 | static void _initFallbacks() {
56 | //DO-NOT-REMOVE FEATURES_FALLBACKS
57 | // MVP
58 | registerFallbackValue(MockAppInitPresenter());
59 | registerFallbackValue(MockAppInitPresentationModel());
60 | registerFallbackValue(MockAppInitInitialParams());
61 | registerFallbackValue(MockAppInitNavigator());
62 | //DO-NOT-REMOVE MVP_MOCK_FALLBACK_VALUE
63 |
64 | // USE CASES
65 | registerFallbackValue(MockAppInitFailure());
66 | registerFallbackValue(MockAppInitUseCase());
67 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE
68 |
69 | // REPOSITORIES
70 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE
71 |
72 | // STORES
73 | //DO-NOT-REMOVE STORES_MOCK_FALLBACK_VALUE
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/test/features/app_init/pages/app_init_page_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_demo/core/domain/stores/user_store.dart';
3 | import 'package:flutter_demo/dependency_injection/app_component.dart';
4 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart';
5 | import 'package:flutter_demo/features/app_init/app_init_navigator.dart';
6 | import 'package:flutter_demo/features/app_init/app_init_page.dart';
7 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart';
8 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart';
9 | import 'package:flutter_test/flutter_test.dart';
10 | import 'package:mocktail/mocktail.dart';
11 |
12 | import '../../../test_utils/golden_tests_utils.dart';
13 | import '../../../test_utils/test_utils.dart';
14 | import '../mocks/app_init_mock_definitions.dart';
15 | import '../mocks/app_init_mocks.dart';
16 |
17 | Future main() async {
18 | late AppInitPage page;
19 | late AppInitInitialParams initParams;
20 | late AppInitPresentationModel model;
21 | late AppInitPresenter presenter;
22 | late AppInitNavigator navigator;
23 |
24 | void _initMvp() {
25 | initParams = const AppInitInitialParams();
26 | model = AppInitPresentationModel.initial(
27 | initParams,
28 | );
29 | navigator = MockAppInitNavigator();
30 | presenter = AppInitPresenter(
31 | model,
32 | navigator,
33 | AppInitMocks.appInitUseCase,
34 | UserStore(),
35 | );
36 | page = AppInitPage(presenter: presenter);
37 | }
38 |
39 | await screenshotTest(
40 | "app_init_page",
41 | setUp: () async {
42 | _initMvp();
43 | when(() => AppInitMocks.appInitUseCase.execute()).thenAnswer((_) => successFuture(unit));
44 | },
45 | pageBuilder: () => page,
46 | );
47 |
48 | test("getIt page resolves successfully", () async {
49 | _initMvp();
50 | final page = getIt(param1: initParams);
51 | expect(page.presenter, isNotNull);
52 | expect(page, isNotNull);
53 | });
54 | }
55 |
--------------------------------------------------------------------------------
/test/features/app_init/pages/flutter_test_config.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import '../../../test_utils/test_utils.dart';
4 |
5 | Future testExecutable(FutureOr Function() testMain) => preparePageTests(testMain);
6 |
--------------------------------------------------------------------------------
/test/features/app_init/pages/goldens/ci/app_init_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/test/features/app_init/pages/goldens/ci/app_init_page.png
--------------------------------------------------------------------------------
/test/features/app_init/pages/goldens/macos/app_init_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/test/features/app_init/pages/goldens/macos/app_init_page.png
--------------------------------------------------------------------------------
/test/features/app_init/presenters/app_init_presenter_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:bloc_test/bloc_test.dart';
2 | import 'package:dartz/dartz.dart';
3 | import 'package:flutter_demo/core/domain/model/app_init_failure.dart';
4 | import 'package:flutter_demo/core/domain/model/user.dart';
5 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart';
6 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart';
7 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart';
8 | import 'package:flutter_test/flutter_test.dart';
9 | import 'package:mocktail/mocktail.dart';
10 |
11 | import '../../../mocks/mocks.dart';
12 | import '../../../test_utils/test_utils.dart';
13 | import '../mocks/app_init_mock_definitions.dart';
14 | import '../mocks/app_init_mocks.dart';
15 |
16 | void main() {
17 | late AppInitPresentationModel model;
18 | late AppInitPresenter presenter;
19 | late MockAppInitNavigator navigator;
20 |
21 | test(
22 | 'should call appInitUseCase on start',
23 | () async {
24 | // GIVEN
25 | whenListen(
26 | Mocks.userStore,
27 | Stream.fromIterable([const User.anonymous()]),
28 | );
29 | when(() => AppInitMocks.appInitUseCase.execute()).thenAnswer((_) => successFuture(unit));
30 |
31 | // WHEN
32 | await presenter.onInit();
33 |
34 | // THEN
35 | verify(() => AppInitMocks.appInitUseCase.execute());
36 | verify(() => Mocks.userStore.stream);
37 | },
38 | );
39 | test(
40 | 'should show error when appInitUseCase fails',
41 | () async {
42 | // GIVEN
43 | whenListen(
44 | Mocks.userStore,
45 | Stream.fromIterable([const User.anonymous()]),
46 | );
47 | when(() => AppInitMocks.appInitUseCase.execute()).thenAnswer((_) => failFuture(const AppInitFailure.unknown()));
48 | when(() => navigator.showError(any())).thenAnswer((_) => Future.value());
49 |
50 | // WHEN
51 | await presenter.onInit();
52 |
53 | // THEN
54 | verify(() => navigator.showError(any()));
55 | },
56 | );
57 |
58 | setUp(() {
59 | model = AppInitPresentationModel.initial(const AppInitInitialParams());
60 | navigator = AppInitMocks.appInitNavigator;
61 | presenter = AppInitPresenter(
62 | model,
63 | navigator,
64 | AppInitMocks.appInitUseCase,
65 | Mocks.userStore,
66 | );
67 | });
68 | }
69 |
--------------------------------------------------------------------------------
/test/features/auth/domain/log_in_use_case_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/core/utils/either_extensions.dart';
2 | import 'package:flutter_demo/dependency_injection/app_component.dart';
3 | import 'package:flutter_demo/features/auth/domain/use_cases/log_in_use_case.dart';
4 | import 'package:flutter_test/flutter_test.dart';
5 |
6 | import '../../../mocks/mocks.dart';
7 |
8 | void main() {
9 | late LogInUseCase useCase;
10 |
11 | setUp(() {
12 | useCase = LogInUseCase(Mocks.userStore);
13 | });
14 |
15 | test(
16 | 'use case executes normally',
17 | () async {
18 | // GIVEN
19 |
20 | // WHEN
21 | final result = await useCase.execute(username: "test", password: "test123");
22 |
23 | // THEN
24 | expect(result.isSuccess, true);
25 | },
26 | );
27 |
28 | test("getIt resolves successfully", () async {
29 | final useCase = getIt();
30 | expect(useCase, isNotNull);
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/test/features/auth/mocks/auth_mock_definitions.dart:
--------------------------------------------------------------------------------
1 | import 'package:bloc_test/bloc_test.dart';
2 | import 'package:flutter_demo/features/auth/domain/model/log_in_failure.dart';
3 | import 'package:flutter_demo/features/auth/domain/use_cases/log_in_use_case.dart';
4 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart';
5 | import 'package:flutter_demo/features/auth/login/login_navigator.dart';
6 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart';
7 | import 'package:flutter_demo/features/auth/login/login_presenter.dart';
8 | import 'package:mocktail/mocktail.dart';
9 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS
10 |
11 | // MVP
12 |
13 | class MockLoginPresenter extends MockCubit implements LoginPresenter {}
14 |
15 | class MockLoginPresentationModel extends Mock implements LoginPresentationModel {}
16 |
17 | class MockLoginInitialParams extends Mock implements LoginInitialParams {}
18 |
19 | class MockLoginNavigator extends Mock implements LoginNavigator {}
20 | //DO-NOT-REMOVE MVP_MOCK_DEFINITION
21 |
22 | // USE CASES
23 | class MockLogInFailure extends Mock implements LogInFailure {}
24 |
25 | class MockLogInUseCase extends Mock implements LogInUseCase {}
26 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION
27 |
28 | // REPOSITORIES
29 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION
30 |
31 | // STORES
32 | //DO-NOT-REMOVE STORES_MOCK_DEFINITION
33 |
--------------------------------------------------------------------------------
/test/features/auth/mocks/auth_mocks.dart:
--------------------------------------------------------------------------------
1 | import 'package:mocktail/mocktail.dart';
2 |
3 | import 'auth_mock_definitions.dart';
4 | //DO-NOT-REMOVE IMPORTS_MOCKS
5 |
6 | class AuthMocks {
7 | // MVP
8 |
9 | static late MockLoginPresenter loginPresenter;
10 | static late MockLoginPresentationModel loginPresentationModel;
11 | static late MockLoginInitialParams loginInitialParams;
12 | static late MockLoginNavigator loginNavigator;
13 | //DO-NOT-REMOVE MVP_MOCKS_STATIC_FIELD
14 |
15 | // USE CASES
16 |
17 | static late MockLogInFailure logInFailure;
18 | static late MockLogInUseCase logInUseCase;
19 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD
20 |
21 | // REPOSITORIES
22 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD
23 |
24 | // STORES
25 |
26 | //DO-NOT-REMOVE STORES_MOCKS_STATIC_FIELD
27 |
28 | static void init() {
29 | _initMocks();
30 | _initFallbacks();
31 | }
32 |
33 | static void _initMocks() {
34 | //DO-NOT-REMOVE FEATURES_MOCKS
35 | // MVP
36 | loginPresenter = MockLoginPresenter();
37 | loginPresentationModel = MockLoginPresentationModel();
38 | loginInitialParams = MockLoginInitialParams();
39 | loginNavigator = MockLoginNavigator();
40 | //DO-NOT-REMOVE MVP_INIT_MOCKS
41 |
42 | // USE CASES
43 | logInFailure = MockLogInFailure();
44 | logInUseCase = MockLogInUseCase();
45 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS
46 |
47 | // REPOSITORIES
48 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS
49 |
50 | // STORES
51 | //DO-NOT-REMOVE STORES_INIT_MOCKS
52 | }
53 |
54 | static void _initFallbacks() {
55 | //DO-NOT-REMOVE FEATURES_FALLBACKS
56 | // MVP
57 | registerFallbackValue(MockLoginPresenter());
58 | registerFallbackValue(MockLoginPresentationModel());
59 | registerFallbackValue(MockLoginInitialParams());
60 | registerFallbackValue(MockLoginNavigator());
61 | //DO-NOT-REMOVE MVP_MOCK_FALLBACK_VALUE
62 |
63 | // USE CASES
64 | registerFallbackValue(MockLogInFailure());
65 | registerFallbackValue(MockLogInUseCase());
66 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE
67 |
68 | // REPOSITORIES
69 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE
70 |
71 | // STORES
72 | //DO-NOT-REMOVE STORES_MOCK_FALLBACK_VALUE
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/test/features/auth/pages/flutter_test_config.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import '../../../test_utils/test_utils.dart';
4 |
5 | Future testExecutable(FutureOr Function() testMain) => preparePageTests(testMain);
6 |
--------------------------------------------------------------------------------
/test/features/auth/pages/goldens/ci/login_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/test/features/auth/pages/goldens/ci/login_page.png
--------------------------------------------------------------------------------
/test/features/auth/pages/goldens/macos/login_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/test/features/auth/pages/goldens/macos/login_page.png
--------------------------------------------------------------------------------
/test/features/auth/pages/login_page_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/dependency_injection/app_component.dart';
2 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart';
3 | import 'package:flutter_demo/features/auth/login/login_navigator.dart';
4 | import 'package:flutter_demo/features/auth/login/login_page.dart';
5 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart';
6 | import 'package:flutter_demo/features/auth/login/login_presenter.dart';
7 | import 'package:flutter_test/flutter_test.dart';
8 |
9 | import '../../../mocks/mocks.dart';
10 | import '../../../test_utils/golden_tests_utils.dart';
11 |
12 | Future main() async {
13 | late LoginPage page;
14 | late LoginInitialParams initParams;
15 | late LoginPresentationModel model;
16 | late LoginPresenter presenter;
17 | late LoginNavigator navigator;
18 |
19 | void _initMvp() {
20 | initParams = const LoginInitialParams();
21 | model = LoginPresentationModel.initial(
22 | initParams,
23 | );
24 | navigator = LoginNavigator(Mocks.appNavigator);
25 | presenter = LoginPresenter(
26 | model,
27 | navigator,
28 | );
29 | page = LoginPage(presenter: presenter);
30 | }
31 |
32 | await screenshotTest(
33 | "login_page",
34 | setUp: () async {
35 | _initMvp();
36 | },
37 | pageBuilder: () => page,
38 | );
39 |
40 | test("getIt page resolves successfully", () async {
41 | _initMvp();
42 | final page = getIt(param1: initParams);
43 | expect(page.presenter, isNotNull);
44 | expect(page, isNotNull);
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/test/features/auth/presenters/login_presenter_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart';
2 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart';
3 | import 'package:flutter_demo/features/auth/login/login_presenter.dart';
4 | import 'package:flutter_test/flutter_test.dart';
5 |
6 | import '../mocks/auth_mock_definitions.dart';
7 |
8 | void main() {
9 | late LoginPresentationModel model;
10 | late LoginPresenter presenter;
11 | late MockLoginNavigator navigator;
12 |
13 | test(
14 | 'sample test',
15 | () {
16 | expect(presenter, isNotNull); // TODO implement this
17 | },
18 | );
19 |
20 | setUp(() {
21 | model = LoginPresentationModel.initial(const LoginInitialParams());
22 | navigator = MockLoginNavigator();
23 | presenter = LoginPresenter(
24 | model,
25 | navigator,
26 | );
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/test/flutter_test_config.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'test_utils/test_utils.dart';
4 |
5 | Future testExecutable(FutureOr Function() testMain) async {
6 | await prepareAppForUnitTests();
7 | return testMain();
8 | }
9 |
--------------------------------------------------------------------------------
/test/mocks/mock_definitions.dart:
--------------------------------------------------------------------------------
1 | import 'package:bloc_test/bloc_test.dart';
2 | import 'package:flutter_demo/core/domain/model/user.dart';
3 | import 'package:flutter_demo/core/domain/stores/user_store.dart';
4 | import 'package:flutter_demo/core/utils/current_time_provider.dart';
5 | import 'package:flutter_demo/core/utils/debouncer.dart';
6 | import 'package:flutter_demo/core/utils/periodic_task_executor.dart';
7 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS
8 |
9 | import 'package:flutter_demo/navigation/app_navigator.dart';
10 | import 'package:mocktail/mocktail.dart';
11 |
12 | class MockAppNavigator extends Mock implements AppNavigator {}
13 |
14 | // MVP
15 |
16 | //DO-NOT-REMOVE MVP_MOCK_DEFINITION
17 |
18 | // USE CASES
19 |
20 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION
21 |
22 | // REPOSITORIES
23 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION
24 |
25 | // STORES
26 | class MockUserStore extends MockCubit implements UserStore {}
27 | //DO-NOT-REMOVE STORES_MOCK_DEFINITION
28 |
29 | class MockDebouncer extends Mock implements Debouncer {}
30 |
31 | class MockPeriodicTaskExecutor extends Mock implements PeriodicTaskExecutor {}
32 |
33 | class MockCurrentTimeProvider extends Mock implements CurrentTimeProvider {}
34 |
--------------------------------------------------------------------------------
/test/mocks/mocks.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_demo/core/domain/model/displayable_failure.dart';
3 | import 'package:flutter_demo/core/utils/periodic_task_executor.dart';
4 | import 'package:flutter_demo/navigation/app_navigator.dart';
5 | import 'package:mocktail/mocktail.dart';
6 |
7 | import '../features/app_init/mocks/app_init_mocks.dart';
8 | import '../features/auth/mocks/auth_mocks.dart';
9 | //DO-NOT-REMOVE IMPORTS_MOCKS
10 |
11 | import 'mock_definitions.dart';
12 |
13 | class Mocks {
14 | static late MockAppNavigator appNavigator;
15 |
16 | // MVP
17 |
18 | //DO-NOT-REMOVE MVP_MOCKS_STATIC_FIELD
19 |
20 | // USE CASES
21 |
22 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD
23 |
24 | // REPOSITORIES
25 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD
26 |
27 | // STORES
28 | static late MockUserStore userStore;
29 |
30 | //DO-NOT-REMOVE STORES_MOCKS_STATIC_FIELD
31 |
32 | static late MockDebouncer debouncer;
33 | static late MockPeriodicTaskExecutor periodicTaskExecutor;
34 | static late MockCurrentTimeProvider currentTimeProvider;
35 |
36 | static void init() {
37 | AppInitMocks.init();
38 | AuthMocks.init();
39 | //DO-NOT-REMOVE FEATURE_MOCKS_INIT
40 |
41 | _initMocks();
42 | _initFallbacks();
43 | }
44 |
45 | static void _initMocks() {
46 | //DO-NOT-REMOVE FEATURES_MOCKS
47 | appNavigator = MockAppNavigator();
48 | // MVP
49 | //DO-NOT-REMOVE MVP_INIT_MOCKS
50 |
51 | // USE CASES
52 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS
53 |
54 | // REPOSITORIES
55 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS
56 |
57 | // STORES
58 | userStore = MockUserStore();
59 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS
60 |
61 | debouncer = MockDebouncer();
62 | periodicTaskExecutor = MockPeriodicTaskExecutor();
63 | currentTimeProvider = MockCurrentTimeProvider();
64 | }
65 |
66 | static void _initFallbacks() {
67 | //DO-NOT-REMOVE FEATURES_FALLBACKS
68 | registerFallbackValue(DisplayableFailure(title: "", message: ""));
69 | // MVP
70 | //DO-NOT-REMOVE MVP_MOCK_FALLBACK_VALUE
71 |
72 | // USE CASES
73 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE
74 |
75 | // REPOSITORIES
76 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE
77 |
78 | // STORES
79 | registerFallbackValue(MockUserStore());
80 | //DO-NOT-REMOVE STORES_MOCK_FALLBACK_VALUE
81 |
82 | registerFallbackValue(materialRoute(Container()));
83 | registerFallbackValue(MockDebouncer());
84 | registerFallbackValue(MockCurrentTimeProvider());
85 | registerFallbackValue(PeriodicTaskExecutor());
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/test/test_utils/golden_test_device_scenario.dart:
--------------------------------------------------------------------------------
1 | import 'package:alchemist/alchemist.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:golden_toolkit/golden_toolkit.dart';
4 |
5 | /// Wrapper for testing widgets (primarily screens) with device constraints
6 | class GoldenTestDeviceScenario extends StatelessWidget {
7 | final Device device;
8 | final ValueGetter builder;
9 |
10 | const GoldenTestDeviceScenario({
11 | required this.builder,
12 | required this.device,
13 | super.key,
14 | });
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return GoldenTestScenario(
19 | name: device.name,
20 | child: ClipRect(
21 | child: MediaQuery(
22 | data: MediaQuery.of(context).copyWith(
23 | size: device.size,
24 | padding: device.safeArea,
25 | platformBrightness: device.brightness,
26 | devicePixelRatio: device.devicePixelRatio,
27 | textScaleFactor: device.textScale,
28 | ),
29 | child: SizedBox(
30 | height: device.size.height,
31 | width: device.size.width,
32 | child: builder(),
33 | ),
34 | ),
35 | ),
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/test_utils/golden_tests_utils.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: unused-code
2 | import 'dart:async';
3 |
4 | import 'package:alchemist/alchemist.dart';
5 | import 'package:flutter/material.dart';
6 | import 'package:golden_toolkit/golden_toolkit.dart';
7 | import 'package:meta/meta.dart';
8 | import 'package:mocktail_image_network/mocktail_image_network.dart';
9 |
10 | import 'golden_test_device_scenario.dart';
11 |
12 | final testDevices = [
13 | Device.phone.copyWith(name: "small phone"),
14 | Device.iphone11.copyWith(name: "iPhone 11"),
15 | ];
16 |
17 | @isTest
18 | Future screenshotTest(
19 | String description, {
20 | String variantName = '',
21 | bool skip = false,
22 | FutureOr Function()? setUp,
23 | required Widget Function() pageBuilder,
24 | List tags = const ['golden'],
25 | List? devices,
26 | Duration timeout = const Duration(seconds: 5),
27 | }) async {
28 | return goldenTest(
29 | description,
30 | fileName: "$description${variantName.trim().isEmpty ? '' : '_$variantName'}",
31 | builder: () {
32 | setUp?.call();
33 |
34 | return GoldenTestGroup(
35 | children: (devices ?? testDevices) //
36 | .map(
37 | (it) => DefaultAssetBundle(
38 | bundle: TestAssetBundle(),
39 | child: GoldenTestDeviceScenario(device: it, builder: pageBuilder),
40 | ),
41 | )
42 | .toList(),
43 | );
44 | },
45 | tags: tags,
46 | skip: skip,
47 | pumpBeforeTest: (tester) => mockNetworkImages(() => precacheImages(tester)).timeout(timeout),
48 | pumpWidget: (tester, widget) => mockNetworkImages(() => tester.pumpWidget(widget)).timeout(timeout),
49 | ).timeout(timeout);
50 | }
51 |
52 | @isTest
53 | Future widgetScreenshotTest(
54 | String description, {
55 | String variantName = '',
56 | bool skip = false,
57 | required WidgetBuilder widgetBuilder,
58 | List tags = const ['golden'],
59 | Duration timeout = const Duration(seconds: 5),
60 | }) async {
61 | return goldenTest(
62 | description,
63 | fileName: "$description${variantName.trim().isEmpty ? '' : '_$variantName'}",
64 | builder: () {
65 | return DefaultAssetBundle(
66 | bundle: TestAssetBundle(),
67 | child: Builder(builder: widgetBuilder),
68 | );
69 | },
70 | tags: tags,
71 | skip: skip,
72 | pumpBeforeTest: (tester) => mockNetworkImages(() => precacheImages(tester)).timeout(timeout),
73 | pumpWidget: (tester, widget) => mockNetworkImages(() => tester.pumpWidget(widget)).timeout(timeout),
74 | ).timeout(timeout);
75 | }
76 |
77 | /// small helper to add container around widget with some background in order to better understand widget's bounds
78 | class TestWidgetContainer extends StatelessWidget {
79 | const TestWidgetContainer({
80 | super.key,
81 | required this.child,
82 | });
83 |
84 | final Widget child;
85 |
86 | @override
87 | Widget build(BuildContext context) {
88 | return DecoratedBox(
89 | decoration: BoxDecoration(
90 | color: Colors.white70,
91 | border: Border.all(color: Colors.red),
92 | ),
93 | child: child,
94 | );
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/test/test_utils/test_utils.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:alchemist/alchemist.dart';
4 | import 'package:dartz/dartz.dart';
5 | import 'package:flutter_demo/core/helpers.dart';
6 | import 'package:flutter_demo/core/utils/either_extensions.dart';
7 | import 'package:flutter_demo/dependency_injection/app_component.dart';
8 | import 'package:flutter_demo/localization/app_localizations_utils.dart';
9 | import 'package:flutter_demo/main.dart';
10 | import 'package:flutter_gen/gen_l10n/app_localizations_en.dart';
11 | import 'package:golden_toolkit/golden_toolkit.dart';
12 |
13 | import '../mocks/mocks.dart';
14 |
15 | Future> successFuture(S result) => Future.value(success(result));
16 |
17 | Future> failFuture(F fail) => Future.value(failure(fail));
18 |
19 | Future prepareAppForUnitTests() async {
20 | isUnitTests = true;
21 | Mocks.init();
22 | notImplemented = ({message, context}) => doNothing();
23 | overrideAppLocalizations(AppLocalizationsEn());
24 | await configureDependenciesForTests();
25 | }
26 |
27 | Future configureDependenciesForTests() async {
28 | await getIt.reset();
29 | configureDependencies();
30 | }
31 |
32 | Future preparePageTests(FutureOr Function() testMain) async {
33 | overrideAppLocalizations(AppLocalizationsEn());
34 | await loadAppFonts();
35 | await prepareAppForUnitTests();
36 | // ignore: do_not_use_environment
37 | const isCi = bool.fromEnvironment('isCI');
38 |
39 | return AlchemistConfig.runWithConfig(
40 | config: const AlchemistConfig(
41 | platformGoldensConfig: PlatformGoldensConfig(
42 | // ignore: avoid_redundant_argument_values
43 | enabled: !isCi,
44 | ),
45 | ),
46 | run: () async {
47 | return testMain();
48 | },
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/tools/arb_files_validator/bin/arb_files_validator.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | Future main(List arguments) async {
4 | var directory = Directory(arguments[0]);
5 | var arbFiles = directory
6 | .list() //
7 | .where((entity) => entity.existsSync() && entity.path.endsWith(".arb"));
8 | var hasDuplicates = false;
9 | print("checking arb files validity...");
10 | await for (final file in arbFiles) {
11 | print("checking '${file.path}'");
12 | final duplicates = _findDuplicateKeys(File(file.path).readAsStringSync());
13 | if (duplicates.isNotEmpty) {
14 | hasDuplicates = true;
15 | print("\n## ERROR: duplicates found! [${duplicates.join(",")}]\n");
16 | }
17 | }
18 | if (hasDuplicates) {
19 | print("duplicates were found in arb file(s) - see logs above\n");
20 | } else {
21 | print("no duplicates found, yay! :)");
22 | }
23 | exit(hasDuplicates ? 1 : 0);
24 | }
25 |
26 | List _findDuplicateKeys(String readAsStringSync) {
27 | final allElems = _extractStringKeys(readAsStringSync);
28 |
29 | final tempList = [];
30 | final duplicates = [];
31 | for (final item in allElems) {
32 | if (tempList.contains(item)) {
33 | duplicates.add(item);
34 | } else {
35 | tempList.add(item);
36 | }
37 | }
38 |
39 | return duplicates;
40 | }
41 |
42 | List _extractStringKeys(String json) {
43 | var temp = json.trim().replaceFirst("{", "").trim();
44 | temp = temp
45 | .substring(0, temp.lastIndexOf("}"))
46 | .replaceAll(RegExp(": \"[^\"]*\""), ": \"\"")
47 | .split("\n") //
48 | .map((e) => e.trim())
49 | .join("")
50 | .trim();
51 | var trimmed = temp;
52 |
53 | do {
54 | temp = trimmed;
55 | trimmed = _removeNestedObjects(temp);
56 | } while (trimmed != temp);
57 | final keys = trimmed
58 | .split(",")
59 | .map((e) {
60 | var match = RegExp("[\\'\\\"](.*?)[\\'\\\"]\\s?:\\s?[\\'\\\"](.*?)[\\'\\\"]").firstMatch(e);
61 | if ((match?.groupCount ?? 0) >= 2) {
62 | return match?.group(1)?.trim();
63 | } else {
64 | return null;
65 | }
66 | })
67 | .whereType()
68 | .toList();
69 | return keys;
70 | }
71 |
72 | String _removeNestedObjects(String replaceAll) {
73 | return replaceAll.replaceAll(RegExp("{[^{}]*}"), "\"\"");
74 | }
75 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lint/analysis_options.yaml
2 |
3 | analyzer:
4 | plugins:
5 | - dart_code_metrics
6 | - custom_lint
7 | exclude:
8 |
9 | language:
10 | strict-raw-types: true
11 |
12 | strong-mode:
13 | implicit-dynamic: true
14 | errors:
15 | invalid_annotation_target: error
16 | argument_type_not_assignable: error
17 | field_initializer_not_assignable: error
18 | map_value_type_not_assignable: error
19 | invalid_assignment: error
20 | return_of_invalid_type_from_closure: error
21 | return_of_invalid_type: error
22 | unnecessary_new: warning
23 | sort_pub_dependencies: ignore
24 | avoid_setters_without_getters: ignore
25 | import_of_legacy_library_into_null_safe: error
26 | avoid_single_cascade_in_expression_statements: ignore
27 | null_aware_in_logical_operator: error
28 | missing_required_param: error
29 | implicit_dynamic_map_literal: ignore
30 | prefer_single_quotes: ignore
31 | missing_return: error
32 | always_declare_return_types: error
33 | override_on_non_overriding_member: error
34 | annotate_overrides: error
35 | avoid_relative_lib_imports: error
36 | avoid_empty_else: error
37 | avoid_returning_null_for_future: error
38 | empty_statements: error
39 | always_put_control_body_on_new_line: error
40 | always_require_non_null_named_parameters: error
41 | avoid_renaming_method_parameters: error
42 | avoid_void_async: error
43 | parameter_assignments: error
44 | constant_identifier_names: ignore
45 | unawaited_futures: error
46 | non_constant_identifier_names: ignore
47 | only_throw_errors: error
48 | exhaustive_cases: error
49 | always_use_package_imports: error
50 | missing_enum_constant_in_switch: error
51 | prefer_const_constructors: error
52 | depend_on_referenced_packages: ignore
53 | use_setters_to_change_properties: ignore
54 | avoid_classes_with_only_static_members: ignore
55 | avoid_positional_boolean_parameters: error
56 | avoid_dynamic_calls: error
57 | require_trailing_commas: error
58 |
59 | linter:
60 | rules:
61 | - avoid_unnecessary_containers
62 | - no_logic_in_create_state
63 | - constant_identifier_names
64 | - prefer_const_constructors
65 | - prefer_const_constructors_in_immutables
66 | - prefer_const_declarations
67 | - prefer_const_literals_to_create_immutables
68 | - annotate_overrides
69 | - await_only_futures
70 | - camel_case_types
71 | - cancel_subscriptions
72 | - close_sinks
73 | - comment_references
74 | - control_flow_in_finally
75 | - empty_statements
76 | - always_declare_return_types
77 | - avoid_empty_else
78 | - avoid_relative_lib_imports
79 | - avoid_returning_null_for_future
80 | - always_put_control_body_on_new_line
81 | - always_require_non_null_named_parameters
82 | - avoid_renaming_method_parameters
83 | - avoid_void_async
84 | - parameter_assignments
85 | - file_names
86 | - empty_constructor_bodies
87 | - unnecessary_parenthesis
88 | - unnecessary_overrides
89 | - use_rethrow_when_possible
90 | - always_use_package_imports
91 | - avoid_init_to_null
92 | - avoid_null_checks_in_equality_operators
93 | - avoid_return_types_on_setters
94 | - avoid_shadowing_type_parameters
95 | - avoid_types_as_parameter_names
96 | - camel_case_extensions
97 | - curly_braces_in_flow_control_structures
98 | - empty_catches
99 | - library_names
100 | - library_prefixes
101 | - no_duplicate_case_values
102 | - null_closures
103 | - omit_local_variable_types
104 | - prefer_adjacent_string_concatenation
105 | - prefer_collection_literals
106 | - prefer_conditional_assignment
107 | - prefer_contains
108 | - prefer_equal_for_default_values
109 | - prefer_final_fields
110 | - prefer_for_elements_to_map_fromIterable
111 | - prefer_generic_function_type_aliases
112 | - prefer_if_null_operators
113 | - prefer_is_empty
114 | - prefer_is_not_empty
115 | - prefer_iterable_whereType
116 | - prefer_single_quotes
117 | - prefer_spread_collections
118 | - recursive_getters
119 | - slash_for_doc_comments
120 | - type_init_formals
121 | - unawaited_futures
122 | - unnecessary_const
123 | - unnecessary_new
124 | - unnecessary_null_in_if_null_operators
125 | - unnecessary_this
126 | - unrelated_type_equality_checks
127 | - use_function_type_syntax_for_parameters
128 | - valid_regexps
129 | - exhaustive_cases
130 | - require_trailing_commas
131 |
132 | dart_code_metrics:
133 | anti-patterns:
134 | - long-method
135 | - long-parameter-list
136 | metrics:
137 | cyclomatic-complexity: 15
138 | maximum-nesting-level: 3
139 | number-of-parameters: 4
140 | source-lines-of-code: 30
141 | metrics-exclude:
142 | - "test/**"
143 | - "widgetbook/**"
144 | - "**/*.gen.dart"
145 |
146 | rules-exclude:
147 | - "test/**"
148 | - "widgetbook/**"
149 | - "**/*.gen.dart"
150 | rules:
151 | - no-boolean-literal-compare
152 | - no-empty-block
153 | - prefer-trailing-comma:
154 | break-on: 3
155 | - prefer-conditional-expressions
156 | - no-equal-then-else
157 | - avoid-unnecessary-type-casts
158 | - avoid-unnecessary-type-assertions
159 | - no-magic-number
160 | - prefer-first
161 | - prefer-last
162 | - prefer-match-file-name
163 | - avoid-use-expanded-as-spacer
164 | - prefer-extracting-callbacks
165 | - prefer-async-await
166 | - prefer-moving-to-variable
167 | - avoid-returning-widgets
168 | - prefer-correct-identifier-length:
169 | exceptions: [ 'i' ]
170 | max-identifier-length: 40
171 | min-identifier-length: 2
172 | - prefer-correct-type-name:
173 | min-length: 2
174 | max-length: 40
175 | - prefer-single-widget-per-file:
176 | ignore-private-widgets: true
177 | - member-ordering:
178 | order:
179 | - constructors
180 | - public-fields
181 | - private-fields
182 | - public-getters
183 | - private-getters
184 | - public-methods
185 | - private-methods
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/custom_lint.dart:
--------------------------------------------------------------------------------
1 | import 'dart:isolate';
2 |
3 | import 'package:analyzer/dart/analysis/results.dart';
4 | import 'package:custom_lint_builder/custom_lint_builder.dart';
5 |
6 | import 'lints/domain_entity_missing_copy_with_method_lint.dart';
7 | import 'lints/domain_entity_missing_empty_constructor_lint.dart';
8 | import 'lints/domain_entity_missing_equatable_lint.dart';
9 | import 'lints/domain_entity_missing_props_items_lint.dart';
10 | import 'lints/domain_entity_non_final_fields_lint.dart';
11 | import 'lints/domain_entity_too_many_public_members_lint.dart';
12 | import 'lints/dont_use_datetime_now_lint.dart';
13 | import 'lints/forbidden_import_in_domain_lint.dart';
14 | import 'lints/forbidden_import_in_presentation_lint.dart';
15 | import 'lints/page_too_widgets.dart';
16 | import 'lints/presentation_model_non_final_field_lint.dart';
17 | import 'lints/presentation_model_structure_lint.dart';
18 | import 'lints/use_case_multiple_accessors_lint.dart';
19 |
20 | void main(List args, SendPort sendPort) {
21 | startPlugin(sendPort, _IndexPlugin());
22 | }
23 |
24 | class _IndexPlugin extends PluginBase {
25 | final lints = [
26 | ForbiddenImportInPresentationLint(),
27 | ForbiddenImportInDomainLint(),
28 | PresentationModelNonFinalFieldLint(),
29 | DomainEntityNonFinalFields(),
30 | UseCaseMultipleAccessorsLint(),
31 | DomainEntityMissingCopyWithMethodLint(),
32 | DomainEntityMissingEquatableLint(),
33 | DomainEntityMissingPropsItemsLint(),
34 | DomainEntityMissingEmptyConstructorLint(),
35 | DomainEntityTooManyPublicMembersLint(),
36 | PageTooManyWidgetsLint(),
37 | DontUseDateTimeNowLint(),
38 | PresentationModelStructureLint(),
39 | ];
40 |
41 | @override
42 | Stream getLints(ResolvedUnitResult resolvedUnitResult) async* {
43 | for (final lint in lints) {
44 | yield* lint.getLints(resolvedUnitResult);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_missing_copy_with_method_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:custom_lint_builder/custom_lint_builder.dart';
3 |
4 | import '../utils/lint_codes.dart';
5 | import '../utils/lint_utils.dart';
6 |
7 | /// checks whether any domain entity class (anything inside 'domain/model' folder except Failures)
8 | /// contains the 'copyWith' method
9 | class DomainEntityMissingCopyWithMethodLint extends PluginBase {
10 | @override
11 | Stream getLints(ResolvedUnitResult unit) async* {
12 | final library = unit.libraryElement;
13 | if (!library.isDomainEntityFile) {
14 | return;
15 | }
16 | final entitiesClasses = library.domainEntitiesClasses;
17 | for (final clazz in entitiesClasses) {
18 | final equatableFields = clazz.equatableFields;
19 | final missingCopyWith = clazz.getMethod("copyWith") == null;
20 |
21 | if (equatableFields.isNotEmpty && missingCopyWith) {
22 | yield Lint(
23 | code: LintCodes.missingCopyWithMethod,
24 | message: 'Domain entity is missing "copyWith" method: "${clazz.displayName}"',
25 | location: clazz.nameLintLocation!,
26 | severity: LintSeverity.error,
27 | correction: "Add a copyWith method, ideally generated with"
28 | " VSCode plugin (Dart Data Class Generator)"
29 | " or IntelliJ Plugin (Dart Data Class)",
30 | );
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_missing_empty_constructor_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:custom_lint_builder/custom_lint_builder.dart';
3 |
4 | import '../utils/lint_codes.dart';
5 | import '../utils/lint_utils.dart';
6 |
7 | /// checks whether any domain entity class (anything inside 'domain/model' folder except Failures)
8 | /// contains the non-parametrized named constructor called 'empty' that creates
9 | /// empty instance of the class with default values
10 | class DomainEntityMissingEmptyConstructorLint extends PluginBase {
11 | @override
12 | Stream getLints(ResolvedUnitResult unit) async* {
13 | final library = unit.libraryElement;
14 | if (!library.isDomainEntityFile) {
15 | return;
16 | }
17 | final entitiesClasses = library.domainEntitiesClasses;
18 | for (final clazz in entitiesClasses) {
19 | final constructor = clazz.getNamedConstructor("empty");
20 | if (constructor == null) {
21 | yield Lint(
22 | code: LintCodes.missingEmptyConstructor,
23 | message: 'Domain entity is missing "empty" constructor',
24 | location: clazz.nameLintLocation!,
25 | severity: LintSeverity.error,
26 | );
27 | } else if (constructor.parameters.isNotEmpty) {
28 | yield Lint(
29 | code: LintCodes.emptyConstructorContainsParams,
30 | message: '"empty" constructor contains params, but it shouldn\'t have any',
31 | location: constructor.nameLintLocation!,
32 | severity: LintSeverity.error,
33 | correction: "Add a non-parametrized, named constructor 'empty()'"
34 | " that sets all fields to their default values "
35 | "like 0, null, '' or \$CLASS_NAME\$.empty()",
36 | );
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_missing_equatable_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:custom_lint_builder/custom_lint_builder.dart';
3 |
4 | import '../utils/lint_codes.dart';
5 | import '../utils/lint_utils.dart';
6 |
7 | /// checks whether domain entity class (anything inside 'domain/model' folder except Failures)
8 | /// extemds Equatable
9 | class DomainEntityMissingEquatableLint extends PluginBase {
10 | @override
11 | Stream getLints(ResolvedUnitResult unit) async* {
12 | final library = unit.libraryElement;
13 | if (!library.isDomainEntityFile) {
14 | return;
15 | }
16 | final entitiesClasses = library.domainEntitiesClasses;
17 | for (final clazz in entitiesClasses) {
18 | if (!clazz.allSupertypes.any((it) => it.element.name == 'Equatable' || it.element.name == 'EquatableMixin')) {
19 | yield Lint(
20 | code: LintCodes.missingEquatable,
21 | message: 'Domain entity is not extending Equatable',
22 | location: clazz.nameLintLocation!,
23 | severity: LintSeverity.error,
24 | correction: "make ${clazz.name} extend from Equatable",
25 | );
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_missing_props_items_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:custom_lint_builder/custom_lint_builder.dart';
3 |
4 | import '../utils/lint_codes.dart';
5 | import '../utils/lint_utils.dart';
6 |
7 | /// checks whether domain entity class (anything inside 'domain/model' folder except Failures)
8 | /// has all its fields listed in the `props` list used by Equatable to generate `==` operator
9 | class DomainEntityMissingPropsItemsLint extends PluginBase {
10 | @override
11 | Stream getLints(ResolvedUnitResult unit) async* {
12 | final library = unit.libraryElement;
13 | if (!library.isDomainEntityFile) {
14 | return;
15 | }
16 | final entitiesClasses = library.domainEntitiesClasses;
17 | for (final clazz in entitiesClasses) {
18 | final props = clazz.getGetter("props");
19 | final propsListElems = clazz.propsListElements;
20 | final equatableFields = clazz.equatableFields;
21 |
22 | if (props != null && equatableFields.length > propsListElems.length) {
23 | final missingFields = equatableFields //
24 | .where((field) => !propsListElems.any((it) => it == field.name))
25 | .map((e) => e.name)
26 | .toList();
27 | yield Lint(
28 | code: LintCodes.missingPropsItems,
29 | message: 'props list is missing some fields: $missingFields',
30 | location: unit.lintLocationFromOffset(props.variable.nameOffset, length: props.variable.nameLength),
31 | severity: LintSeverity.error,
32 | correction: "add '${missingFields.join("', '").replaceAll(", '\$", "")} to the `props` list.",
33 | );
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_non_final_fields_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:custom_lint_builder/custom_lint_builder.dart';
3 |
4 | import '../utils/lint_codes.dart';
5 | import '../utils/lint_utils.dart';
6 |
7 | /// checks whether domain entity class (anything inside 'domain/model' folder except Failures)
8 | /// has all its fields final (making it immutable)
9 | class DomainEntityNonFinalFields extends PluginBase {
10 | @override
11 | Stream getLints(ResolvedUnitResult unit) async* {
12 | final library = unit.libraryElement;
13 | if (!library.isDomainEntityFile) {
14 | return;
15 | }
16 | final entitiesClasses = library.domainEntitiesClasses;
17 | for (final clazz in entitiesClasses) {
18 | final invalidFields = clazz.fields.where((it) => !it.isFinal && !it.isStatic && !it.isConst);
19 |
20 | for (final field in invalidFields) {
21 | if (field.nameLintLocation != null) {
22 | yield Lint(
23 | code: LintCodes.nonFinalFieldInDomainEntity,
24 | message: 'non-final field in domain entity',
25 | location: field.nameLintLocation!,
26 | severity: LintSeverity.error,
27 | correction: "make '${field.name}' field final",
28 | getAnalysisErrorFixes: field.addFinalErrorFix,
29 | );
30 | }
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_too_many_public_members_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:analyzer/dart/element/element.dart';
3 | import 'package:custom_lint_builder/custom_lint_builder.dart';
4 | import 'package:recase/recase.dart';
5 |
6 | import '../utils/lint_codes.dart';
7 | import '../utils/lint_utils.dart';
8 |
9 | /// checks if domain entity class has only one public member (class,enum, mixin etc.)
10 | class DomainEntityTooManyPublicMembersLint extends PluginBase {
11 | @override
12 | Stream getLints(ResolvedUnitResult unit) async* {
13 | final library = unit.libraryElement;
14 | if (!library.isDomainEntityFile || library.isFailureFile) {
15 | return;
16 | }
17 | final entitiesPublicMembers = library.topLevelElements.whereType().where(
18 | (it) => [
19 | !it.name.endsWith("Failure"),
20 | it.isPublic,
21 | !library.source.shortName.startsWith(it.name.snakeCase),
22 | ].every((it) => it),
23 | );
24 |
25 | for (final clazz in entitiesPublicMembers) {
26 | yield Lint(
27 | code: LintCodes.tooManyPublicMembers,
28 | message: "Domain entity's file contains more than one public top level element "
29 | "or the element does not match the file name precisely: ${clazz.name}",
30 | location: clazz.nameLintLocation!,
31 | severity: LintSeverity.error,
32 | correction: "extract ${clazz.name} to separate file",
33 | );
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/dont_use_datetime_now_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:custom_lint_builder/custom_lint_builder.dart';
3 |
4 | import '../utils/lint_codes.dart';
5 |
6 | /// prevents from using `DateTime.now` anywhere in the code
7 | class DontUseDateTimeNowLint extends PluginBase {
8 | final forbiddenText = "DateTime.now()";
9 |
10 | @override
11 | Stream getLints(ResolvedUnitResult unit) async* {
12 | final library = unit.libraryElement;
13 | final source = library.source.contents.data;
14 | var index = 0;
15 | final locations = [];
16 | while (index != -1) {
17 | index = source.indexOf(forbiddenText, index + 1);
18 | if (index != -1) {
19 | locations.add(unit.lintLocationFromOffset(index, length: forbiddenText.length));
20 | }
21 | }
22 | for (final location in locations) {
23 | yield Lint(
24 | code: LintCodes.noDateTimeNow,
25 | message: "Don't use $forbiddenText in code, use `CurrentTimeProvider` instead",
26 | location: location,
27 | severity: LintSeverity.error,
28 | correction: "Use `CurrentTimeProvider` instead",
29 | );
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/forbidden_import_in_domain_lint.dart:
--------------------------------------------------------------------------------
1 | // This is the entrypoint of our custom linter
2 | import 'package:analyzer/dart/analysis/results.dart';
3 | import 'package:collection/collection.dart';
4 | import 'package:custom_lint_builder/custom_lint_builder.dart';
5 |
6 | import '../utils/lint_codes.dart';
7 | import '../utils/lint_utils.dart';
8 |
9 | /// checks whether any domain class (the one that is in `domain` package) contains only allowed imports
10 | /// from [allowedPackages] and does NOT contain any import matching anything from [forbiddenKeywords].
11 | class ForbiddenImportInDomainLint extends PluginBase {
12 | static const allowedPackages = [
13 | "dartz",
14 | "collection",
15 | "equatable", // domain entities
16 | "bloc", //for stores
17 | ];
18 | static const forbiddenKeywords = [
19 | "/data/",
20 | "/widgets/",
21 | "/ui/",
22 | "presenter.dart",
23 | "presentation_model.dart",
24 | "page.dart",
25 | ];
26 |
27 | @override
28 | Stream getLints(ResolvedUnitResult unit) async* {
29 | final appPackage = packageFromUri(unit.uri.toString());
30 |
31 | final library = unit.libraryElement;
32 |
33 | if (appPackage.isEmpty || !unit.uri.toString().contains("domain")) {
34 | return;
35 | }
36 |
37 | final imports = library.nonCoreImports //
38 | .map((it) => MapEntry(it, it.importedLibrary))
39 | .where((entry) {
40 | final importUri = entry.value?.source.uri.toString() ?? '';
41 | final importPackage = packageFromUri(importUri);
42 | final onlyAllowedPackages = [...allowedPackages, appPackage].contains(importPackage);
43 | final noForbiddenImports = forbiddenKeywords.none((it) => importUri.contains(it));
44 | return !onlyAllowedPackages || !noForbiddenImports;
45 | }).toList();
46 | if (imports.isEmpty) {
47 | return;
48 | }
49 |
50 | for (final entry in imports) {
51 | yield Lint(
52 | code: LintCodes.forbiddenImportInDomain,
53 | message: 'domain can only import app packages or following libraries: "${allowedPackages.join(", ")}"'
54 | ' and cannot contain following keywords : "${forbiddenKeywords.join(", ")}"',
55 | location: unit.lintLocationFromOffset(
56 | entry.key.nameOffset,
57 | length: "import '${entry.key.uri}';".length,
58 | ),
59 | severity: LintSeverity.error,
60 | correction: "remove forbidden imports",
61 | );
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/forbidden_import_in_presentation_lint.dart:
--------------------------------------------------------------------------------
1 | // This is the entrypoint of our custom linter
2 | import 'package:analyzer/dart/analysis/results.dart';
3 | import 'package:custom_lint_builder/custom_lint_builder.dart';
4 |
5 | import '../utils/lint_codes.dart';
6 | import '../utils/lint_utils.dart';
7 |
8 | /// checks whether any presentation class (presenter or presentationModel) contains only allowed imports
9 | /// from [allowedPackages]
10 | class ForbiddenImportInPresentationLint extends PluginBase {
11 | static const allowedPackages = [
12 | "bloc",
13 | "dartz",
14 | "collection",
15 | ];
16 |
17 | @override
18 | Stream getLints(ResolvedUnitResult unit) async* {
19 | final library = unit.libraryElement;
20 | if (!library.source.shortName.endsWith("presenter.dart") &&
21 | !library.source.shortName.endsWith("presentation_model.dart")) {
22 | return;
23 | }
24 |
25 | final appPackage = packageFromUri(unit.uri.toString());
26 | final imports = library.nonCoreImports.map((it) => MapEntry(it, it.importedLibrary)).where((lib) {
27 | final package = packageFromUri(lib.value?.source.uri.toString());
28 | return ![...allowedPackages, appPackage].contains(package);
29 | }).toList();
30 | if (imports.isEmpty) {
31 | return;
32 | }
33 |
34 | for (final entry in imports) {
35 | yield Lint(
36 | code: LintCodes.forbiddenImportInPresentation,
37 | message:
38 | 'presenter and presentation model can only import app packages or following libraries: "${allowedPackages.join(", ")}"',
39 | location: unit.lintLocationFromOffset(
40 | entry.key.nameOffset,
41 | length: "import '${entry.key.uri}';".length,
42 | ),
43 | correction: "remove forbidden imports",
44 | severity: LintSeverity.error,
45 | );
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/page_too_widgets.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:custom_lint_builder/custom_lint_builder.dart';
3 | import 'package:recase/recase.dart';
4 |
5 | import '../utils/lint_codes.dart';
6 |
7 | /// checks if the page file contains more top-level elements than needed
8 | /// (only *Page and `_*PageState classes are expected)
9 | class PageTooManyWidgetsLint extends PluginBase {
10 | @override
11 | Stream getLints(ResolvedUnitResult unit) async* {
12 | final library = unit.libraryElement;
13 | if (!library.source.shortName.endsWith("page.dart")) {
14 | return;
15 | }
16 | final topMembers = library.topLevelElements.where((element) {
17 | final name = element.name ?? "";
18 | return !name.endsWith("State") && !name.endsWith(library.source.shortName.replaceAll(".dart", "").pascalCase);
19 | });
20 | if (topMembers.isNotEmpty) {
21 | print("page public elements: [${topMembers.map((e) => e.name).join(", ")}]");
22 | }
23 | for (final clazz in topMembers) {
24 | yield Lint(
25 | code: LintCodes.tooManyPageElements,
26 | message: "${clazz.name} should not be part of the page's source file "
27 | "or the element does not match the file name precisely: ${clazz.name}",
28 | location: clazz.nameLintLocation!,
29 | severity: LintSeverity.error,
30 | correction: "extract ${clazz.name} to separate file",
31 | );
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/presentation_model_non_final_field_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:analyzer/dart/element/element.dart';
3 | import 'package:custom_lint_builder/custom_lint_builder.dart';
4 |
5 | import '../utils/lint_codes.dart';
6 | import '../utils/lint_utils.dart';
7 |
8 | /// checks whether presentationModel class
9 | /// has all its fields final (making it immutable)
10 | class PresentationModelNonFinalFieldLint extends PluginBase {
11 | @override
12 | Stream getLints(ResolvedUnitResult unit) async* {
13 | final library = unit.libraryElement;
14 | if (!library.source.shortName.endsWith("presentation_model.dart")) {
15 | return;
16 | }
17 | final nonFinalFields = library.topLevelElements
18 | .whereType()
19 | .where((element) => element.name.endsWith("PresentationModel"))
20 | .expand((clazz) => clazz.fields.where((field) => !field.isFinal && !field.isStatic && !field.isConst));
21 |
22 | if (nonFinalFields.isEmpty) {
23 | return;
24 | }
25 |
26 | for (final field in nonFinalFields) {
27 | if (field.nameLintLocation != null) {
28 | yield Lint(
29 | code: LintCodes.presentationModelNonFinalField,
30 | message: 'PresentationModel can have only final fields: "${field.displayName}"',
31 | location: field.nameLintLocation!,
32 | severity: LintSeverity.error,
33 | correction: "make ${field.name} final",
34 | getAnalysisErrorFixes: field.addFinalErrorFix,
35 | );
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/presentation_model_structure_lint.dart:
--------------------------------------------------------------------------------
1 | // This is the entrypoint of our custom linter
2 | import 'package:analyzer/dart/analysis/results.dart';
3 | import 'package:analyzer/dart/element/element.dart';
4 | import 'package:collection/collection.dart';
5 | import 'package:custom_lint_builder/custom_lint_builder.dart';
6 |
7 | import '../utils/lint_codes.dart';
8 | import '../utils/lint_utils.dart';
9 |
10 | /// checks if presentation model contains proper structure
11 | class PresentationModelStructureLint extends PluginBase {
12 | static const allowedPackages = [
13 | "bloc",
14 | "dartz",
15 | "collection",
16 | ];
17 |
18 | @override
19 | Stream getLints(ResolvedUnitResult unit) async* {
20 | final library = unit.libraryElement;
21 | if (!library.source.shortName.endsWith("presentation_model.dart")) {
22 | return;
23 | }
24 |
25 | final modelClass = library.topLevelElements //
26 | .whereType()
27 | .firstWhereOrNull((it) => it.name.endsWith("PresentationModel"));
28 | final constructor = modelClass //
29 | ?.constructors
30 | .firstWhereOrNull((element) => element.name == '_');
31 | final invalidConstructorParams =
32 | constructor?.parameters.where((it) => !it.isRequired || !it.isNamed || it.defaultValueCode != null) ?? [];
33 | if (modelClass != null) {
34 | if (constructor == null) {
35 | yield Lint(
36 | code: LintCodes.presentationModelStructure,
37 | message: 'missing a named constructor `_`',
38 | location: modelClass.nameLintLocation!,
39 | correction: "add named constructor `_` to ${modelClass.name}",
40 | severity: LintSeverity.error,
41 | );
42 | } else if (constructor.parameters.length != modelClass.equatableFields.length) {
43 | final missingFields = modelClass.equatableFields
44 | .where((field) => constructor.parameters.none((param) => param.name == field.name));
45 | yield Lint(
46 | code: LintCodes.presentationModelStructure,
47 | message: 'some fields are missing in the `_` constructor',
48 | location: constructor.nameLintLocation!,
49 | correction: "add ${missingFields.join(", ")} to the constructor",
50 | severity: LintSeverity.error,
51 | );
52 | }
53 | for (final param in invalidConstructorParams) {
54 | yield Lint(
55 | code: LintCodes.presentationModelStructure,
56 | message: '`${param.name}` constructor param is not marked required and/or named, or contains default value',
57 | location: param.nameLintLocation!,
58 | correction: "make `${param.name}` a required, named parameter with no default value",
59 | severity: LintSeverity.error,
60 | );
61 | }
62 | if (modelClass.getMethod("copyWith") == null) {
63 | yield Lint(
64 | code: LintCodes.presentationModelStructure,
65 | message: 'missing `copyWith` method',
66 | location: modelClass.nameLintLocation!,
67 | correction: "add `copyWith` method to ${modelClass.name}",
68 | severity: LintSeverity.error,
69 | );
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/lints/use_case_multiple_accessors_lint.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/analysis/results.dart';
2 | import 'package:analyzer/dart/element/element.dart';
3 | import 'package:custom_lint_builder/custom_lint_builder.dart';
4 |
5 | import '../utils/lint_codes.dart';
6 |
7 | /// checks whether a useCase class contains only one public accessor, a method, called 'execute'
8 | class UseCaseMultipleAccessorsLint extends PluginBase {
9 | @override
10 | Stream getLints(ResolvedUnitResult unit) async* {
11 | final library = unit.libraryElement;
12 | if (!library.source.shortName.endsWith("use_case.dart")) {
13 | return;
14 | }
15 | final useCaseClass = library.topLevelElements.whereType().first;
16 | final publicFields = useCaseClass.fields.where((element) => element.isPublic && !element.isConst);
17 | final publicMethods = useCaseClass.methods.where((element) => element.isPublic);
18 |
19 | for (final field in publicFields) {
20 | if (field.nameLintLocation != null) {
21 | yield Lint(
22 | code: LintCodes.publicFieldInUseCase,
23 | message: 'use case cannot expose public fields, only one execute() method',
24 | location: field.nameLintLocation!,
25 | severity: LintSeverity.error,
26 | );
27 | }
28 | }
29 | for (final method in publicMethods) {
30 | if (method.name != "execute") {
31 | yield Lint(
32 | code: LintCodes.publicMethodInUseCase,
33 | message: 'use case can only expose one public method called "execute"',
34 | location: method.nameLintLocation!,
35 | severity: LintSeverity.error,
36 | correction: "remove or rename ${method.name} to 'execute'",
37 | );
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/utils/lint_codes.dart:
--------------------------------------------------------------------------------
1 | class LintCodes {
2 | static const forbiddenImportInPresentation = 'forbidden_import_in_presentation';
3 | static const forbiddenImportInDomain = 'forbidden_import_in_domain';
4 | static const missingCopyWithMethod = 'missing_copy_with_method';
5 | static const missingEquatable = 'missing_equatable';
6 | static const missingPropsItems = 'missing_props_items';
7 | static const nonFinalFieldInDomainEntity = 'non_final_field_in_domain_entity';
8 | static const presentationModelNonFinalField = 'presentation_model_non_final_field';
9 | static const missingEmptyConstructor = 'missing_empty_constructor';
10 | static const emptyConstructorContainsParams = 'empty_constructor_contains_params';
11 | static const publicFieldInUseCase = 'public_field_in_use_case';
12 | static const publicMethodInUseCase = 'public_method_in_use_case';
13 | static const tooManyPublicMembers = 'too_many_public_members';
14 | static const tooManyPageElements = 'too_many_page_file_members';
15 | static const noDateTimeNow = 'no_date_time_now';
16 | static const presentationModelStructure = 'presentation_model_structure';
17 | }
18 |
--------------------------------------------------------------------------------
/tools/custom_lints/clean_architecture_lints/bin/utils/lint_utils.dart:
--------------------------------------------------------------------------------
1 | import 'package:analyzer/dart/element/element.dart';
2 | import 'package:analyzer_plugin/protocol/protocol_generated.dart';
3 | import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
4 | import 'package:custom_lint_builder/custom_lint_builder.dart';
5 |
6 | /// returns the package name for given uri, for example `package:bloc/bloc.dart` will return `bloc`
7 | String packageFromUri(String? uri) {
8 | if (uri == null) {
9 | return '';
10 | }
11 | try {
12 | return uri.substring(uri.indexOf(":") + 1, uri.indexOf("/"));
13 | } catch (ex) {
14 | return '';
15 | }
16 | }
17 |
18 | /// List of fields that should be part of props list for Equatable class
19 | extension ClassElementExtensions on ClassElement {
20 | Iterable get equatableFields =>
21 | fields.where((field) => !field.isStatic && !field.isAbstract && (field.getter?.isSynthetic ?? false));
22 |
23 | /// Returns list of all elements in the `List