diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index a1f6df5..9dfc1e6 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -13,7 +13,7 @@ jobs: name: "Run analyze" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.6 - uses: subosito/flutter-action@v2.16.0 with: channel: stable diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index ab170a8..435b6ea 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -10,7 +10,7 @@ jobs: name: "Deploy on Github Pages" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.6 - uses: subosito/flutter-action@v2.16.0 with: channel: stable diff --git a/lib/src/feature/app/logic/app_runner.dart b/lib/src/feature/app/logic/app_runner.dart index 7dd0664..aaaf71a 100644 --- a/lib/src/feature/app/logic/app_runner.dart +++ b/lib/src/feature/app/logic/app_runner.dart @@ -29,11 +29,11 @@ final class AppRunner { Bloc.observer = const AppBlocObserver(); Bloc.transformer = bloc_concurrency.sequential(); const config = Config(); - const initializationProcessor = InitializationProcessor(config); + const initializationProcessor = CompositionRoot(config); Future initializeAndRun() async { try { - final result = await initializationProcessor.initialize(); + final result = await initializationProcessor.compose(); // Attach this widget to the root of the tree. runApp(App(result: result)); } on Object catch (e, stackTrace) { diff --git a/lib/src/feature/app/widget/app.dart b/lib/src/feature/app/widget/app.dart index 5cdafd0..551ae91 100644 --- a/lib/src/feature/app/widget/app.dart +++ b/lib/src/feature/app/widget/app.dart @@ -15,9 +15,8 @@ class App extends StatelessWidget { /// {@macro app} const App({required this.result, super.key}); - /// The initialization result from the [InitializationProcessor] - /// which contains initialized dependencies. - final InitializationResult result; + /// The result from the [CompositionRoot]. + final CompositionResult result; @override Widget build(BuildContext context) => DependenciesScope( diff --git a/lib/src/feature/initialization/logic/initialization_processor.dart b/lib/src/feature/initialization/logic/initialization_processor.dart index 398971c..424aa06 100644 --- a/lib/src/feature/initialization/logic/initialization_processor.dart +++ b/lib/src/feature/initialization/logic/initialization_processor.dart @@ -18,16 +18,41 @@ import 'package:wordly/src/feature/settings/data/theme_repository.dart'; import 'package:wordly/src/feature/statistic/data/statistics_datasource.dart'; import 'package:wordly/src/feature/statistic/data/statistics_repository.dart'; -/// {@template initialization_processor} -/// A class which is responsible for processing initialization steps. +/// {@template composition_root} +/// A place where all dependencies are initialized. /// {@endtemplate} -final class InitializationProcessor { - /// {@macro initialization_processor} - const InitializationProcessor(this.config); +/// +/// {@template composition_process} +/// Composition of dependencies is a process of creating and configuring +/// instances of classes that are required for the application to work. +/// +/// It is a good practice to keep all dependencies in one place to make it +/// easier to manage them and to ensure that they are initialized only once. +/// {@endtemplate} +final class CompositionRoot { + /// {@macro composition_root} + const CompositionRoot(this.config); /// Application configuration final Config config; + /// Composes dependencies and returns result of composition. + Future compose() async { + final stopwatch = Stopwatch()..start(); + + logger.info('Initializing dependencies...'); + // initialize dependencies + final dependencies = await _initDependencies(); + logger.info('Dependencies initialized'); + + stopwatch.stop(); + final result = CompositionResult( + dependencies: dependencies, + msSpent: stopwatch.elapsedMilliseconds, + ); + return result; + } + Future _initDependencies() async { final sharedPreferences = await SharedPreferences.getInstance(); final settingsBloc = await _initSettingsBloc(sharedPreferences); @@ -45,7 +70,6 @@ final class InitializationProcessor { StatisticsRepositoryImpl(statisticsDataSource: StatisticsDataSourceLocal(sharedPreferences: sharedPreferences)); return Dependencies( - sharedPreferences: sharedPreferences, settingsBloc: settingsBloc, gameRepository: gameRepository, levelRepository: levelRepository, @@ -84,25 +108,4 @@ final class InitializationProcessor { ); return settingsBloc; } - - /// Initializes dependencies and returns the result of the initialization. - /// - /// This method may contain additional steps that need initialization - /// before the application starts - /// (for example, caching or enabling tracking manager) - Future initialize() async { - final stopwatch = Stopwatch()..start(); - - logger.info('Initializing dependencies...'); - // initialize dependencies - final dependencies = await _initDependencies(); - logger.info('Dependencies initialized'); - - stopwatch.stop(); - final result = InitializationResult( - dependencies: dependencies, - msSpent: stopwatch.elapsedMilliseconds, - ); - return result; - } } diff --git a/lib/src/feature/initialization/model/dependencies.dart b/lib/src/feature/initialization/model/dependencies.dart index 4d1413a..0ee7cb1 100644 --- a/lib/src/feature/initialization/model/dependencies.dart +++ b/lib/src/feature/initialization/model/dependencies.dart @@ -1,38 +1,39 @@ -import 'package:shared_preferences/shared_preferences.dart'; import 'package:wordly/src/feature/game/data/game_repository.dart'; +import 'package:wordly/src/feature/initialization/logic/initialization_processor.dart'; import 'package:wordly/src/feature/level/data/level_repository.dart'; import 'package:wordly/src/feature/settings/bloc/settings_bloc.dart'; import 'package:wordly/src/feature/statistic/data/statistics_repository.dart'; /// {@template dependencies} -/// Dependencies container +/// Composed dependencies from the [CompositionRoot]. +/// +/// This class is used to pass dependencies to the application. +/// +/// {@macro composition_process} /// {@endtemplate} base class Dependencies { /// {@macro dependencies} const Dependencies({ - required this.sharedPreferences, required this.settingsBloc, required this.gameRepository, required this.levelRepository, required this.statisticsRepository, }); - /// [SharedPreferences] instance, used to store Key-Value pairs. - final SharedPreferences sharedPreferences; - - /// [SettingsBloc] instance, used to manage theme and locale. final SettingsBloc settingsBloc; final StatisticsRepository statisticsRepository; final ILevelRepository levelRepository; final IGameRepository gameRepository; } -/// {@template initialization_result} -/// Result of initialization +/// {@template composition_result} +/// Result of composition +/// +/// {@macro composition_process} /// {@endtemplate} -final class InitializationResult { - /// {@macro initialization_result} - const InitializationResult({ +final class CompositionResult { + /// {@macro composition_result} + const CompositionResult({ required this.dependencies, required this.msSpent, }); @@ -44,7 +45,7 @@ final class InitializationResult { final int msSpent; @override - String toString() => '$InitializationResult(' + String toString() => '$CompositionResult(' 'dependencies: $dependencies, ' 'msSpent: $msSpent' ')'; diff --git a/lib/src/feature/settings/bloc/settings_bloc.dart b/lib/src/feature/settings/bloc/settings_bloc.dart index 6d824a4..c0916df 100644 --- a/lib/src/feature/settings/bloc/settings_bloc.dart +++ b/lib/src/feature/settings/bloc/settings_bloc.dart @@ -1,82 +1,12 @@ -import 'package:flutter/material.dart' show Locale; +import 'dart:ui' show Locale; + import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:meta/meta.dart'; import 'package:wordly/src/core/resources/resources.dart'; import 'package:wordly/src/feature/settings/data/dictionary_repository.dart'; import 'package:wordly/src/feature/settings/data/locale_repository.dart'; import 'package:wordly/src/feature/settings/data/theme_repository.dart'; -part 'settings_bloc.freezed.dart'; - -/// States for the [SettingsBloc]. -@Freezed() -sealed class SettingsState with _$SettingsState { - const SettingsState._(); - - /// Idle state for the [SettingsBloc]. - const factory SettingsState.idle({ - /// The current locale. - Locale? locale, - - /// The current dictionary. - Locale? dictionary, - - /// The current theme mode. - AppTheme? appTheme, - }) = _IdleSettingsState; - - /// Processing state for the [SettingsBloc]. - const factory SettingsState.processing({ - /// The current locale. - Locale? locale, - - /// The current dictionary. - Locale? dictionary, - - /// The current theme mode. - AppTheme? appTheme, - }) = _ProcessingSettingsState; - - /// Error state for the [SettingsBloc]. - const factory SettingsState.error({ - /// The error message. - required Object cause, - - /// The current locale. - Locale? locale, - - /// The current dictionary. - Locale? dictionary, - - /// The current theme mode. - AppTheme? appTheme, - }) = _ErrorSettingsState; -} - -/// Events for the [SettingsBloc]. -@Freezed(copyWith: false) -sealed class SettingsEvent with _$SettingsEvent { - const SettingsEvent._(); - - /// Event to update the theme mode. - const factory SettingsEvent.updateTheme({ - /// The new theme mode. - required AppTheme appTheme, - }) = _UpdateThemeSettingsEvent; - - /// Event to update the dictionary. - const factory SettingsEvent.updateDictionary({ - /// The new locale. - required Locale dictionary, - }) = _UpdateDictionarySettingsEvent; - - /// Event to update the locale. - const factory SettingsEvent.updateLocale({ - /// The new locale. - required Locale locale, - }) = _UpdateLocaleSettingsEvent; -} - /// {@template settings_bloc} /// A [Bloc] that handles the settings. /// {@endtemplate} @@ -84,30 +14,27 @@ final class SettingsBloc extends Bloc { /// {@macro settings_bloc} SettingsBloc({ required LocaleRepository localeRepository, - required DictionaryRepository dictionaryRepository, required ThemeRepository themeRepository, + required DictionaryRepository dictionaryRepository, required SettingsState initialState, }) : _localeRepository = localeRepository, - _dictionaryRepository = dictionaryRepository, _themeRepository = themeRepository, + _dictionaryRepository = dictionaryRepository, super(initialState) { on( - (event, emit) async => event.map( - updateTheme: (event) => _updateTheme(event, emit), - updateLocale: (event) => _updateLocale(event, emit), - updateDictionary: (event) => _updateDictionary(event, emit), - ), + (event, emit) async => switch (event) { + final _UpdateLocaleSettingsEvent e => _updateLocale(e, emit), + final _UpdateDictionarySettingsEvent e => _updateDictionary(e, emit), + final _UpdateThemeSettingsEvent e => _updateTheme(e, emit), + }, ); } final LocaleRepository _localeRepository; - final DictionaryRepository _dictionaryRepository; final ThemeRepository _themeRepository; + final DictionaryRepository _dictionaryRepository; - Future _updateTheme( - _UpdateThemeSettingsEvent event, - Emitter emitter, - ) async { + Future _updateTheme(_UpdateThemeSettingsEvent event, Emitter emitter) async { emitter( SettingsState.processing( appTheme: state.appTheme, @@ -135,10 +62,7 @@ final class SettingsBloc extends Bloc { } } - Future _updateDictionary( - _UpdateDictionarySettingsEvent event, - Emitter emitter, - ) async { + Future _updateLocale(_UpdateLocaleSettingsEvent event, Emitter emitter) async { emitter( SettingsState.processing( appTheme: state.appTheme, @@ -148,10 +72,10 @@ final class SettingsBloc extends Bloc { ); try { - await _dictionaryRepository.setDictionary(event.dictionary); + await _localeRepository.setLocale(event.locale); emitter( - SettingsState.idle(appTheme: state.appTheme, locale: state.locale, dictionary: event.dictionary), + SettingsState.idle(appTheme: state.appTheme, locale: event.locale, dictionary: state.dictionary), ); } on Object catch (e) { emitter( @@ -166,10 +90,7 @@ final class SettingsBloc extends Bloc { } } - Future _updateLocale( - _UpdateLocaleSettingsEvent event, - Emitter emitter, - ) async { + Future _updateDictionary(_UpdateDictionarySettingsEvent event, Emitter emitter) async { emitter( SettingsState.processing( appTheme: state.appTheme, @@ -179,14 +100,10 @@ final class SettingsBloc extends Bloc { ); try { - await _localeRepository.setLocale(event.locale); + await _dictionaryRepository.setDictionary(event.dictionary); emitter( - SettingsState.idle( - appTheme: state.appTheme, - locale: event.locale, - dictionary: state.dictionary, - ), + SettingsState.idle(appTheme: state.appTheme, locale: state.locale, dictionary: event.dictionary), ); } on Object catch (e) { emitter( @@ -201,3 +118,194 @@ final class SettingsBloc extends Bloc { } } } + +/// States for the [SettingsBloc]. +sealed class SettingsState { + const SettingsState({this.locale, this.dictionary, this.appTheme}); + + /// Idle state for the [SettingsBloc]. + const factory SettingsState.idle({Locale? locale, Locale? dictionary, AppTheme? appTheme}) = _IdleSettingsState; + + /// Processing state for the [SettingsBloc]. + const factory SettingsState.processing({Locale? locale, Locale? dictionary, AppTheme? appTheme}) = + _ProcessingSettingsState; + + /// Error state for the [SettingsBloc]. + const factory SettingsState.error({ + required Object cause, + Locale? locale, + Locale? dictionary, + AppTheme? appTheme, + }) = _ErrorSettingsState; + + /// Application locale. + final Locale? locale; + + /// Application dictionary. + final Locale? dictionary; + + /// Data class used to represent the state of theme. + final AppTheme? appTheme; +} + +@immutable +final class _IdleSettingsState extends SettingsState { + const _IdleSettingsState({super.locale, super.dictionary, super.appTheme}); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is _IdleSettingsState && + other.locale == locale && + other.dictionary == dictionary && + other.appTheme == appTheme; + } + + @override + int get hashCode => Object.hash(locale, dictionary, appTheme); + + @override + String toString() => 'SettingsState.idle(locale: $locale, dictionary: $dictionary, appTheme: $appTheme)'; +} + +@immutable +final class _ProcessingSettingsState extends SettingsState { + const _ProcessingSettingsState({super.locale, super.dictionary, super.appTheme}); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is _ProcessingSettingsState && + other.locale == locale && + other.dictionary == dictionary && + other.appTheme == appTheme; + } + + @override + int get hashCode => Object.hash(locale, dictionary, appTheme); + + @override + String toString() => 'SettingsState.processing(locale: $locale, dictionary: $dictionary, appTheme: $appTheme)'; +} + +@immutable +final class _ErrorSettingsState extends SettingsState { + const _ErrorSettingsState({ + required this.cause, + super.locale, + super.dictionary, + super.appTheme, + }); + + /// The cause of the error. + final Object cause; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is _ErrorSettingsState && + other.cause == cause && + other.locale == locale && + other.dictionary == dictionary && + other.appTheme == appTheme; + } + + @override + int get hashCode => Object.hash(cause, locale, dictionary, appTheme); + + @override + String toString() => 'SettingsState.error(cause: $cause, ' + 'locale: $locale, dictionary: $dictionary, appTheme: $appTheme)'; +} + +/// Events for the [SettingsBloc]. +sealed class SettingsEvent { + const SettingsEvent(); + + /// Event to update theme. + const factory SettingsEvent.updateTheme({required AppTheme appTheme}) = _UpdateThemeSettingsEvent; + + /// Event to update the locale. + const factory SettingsEvent.updateLocale({required Locale locale}) = _UpdateLocaleSettingsEvent; + + /// Event to update the dictionary. + const factory SettingsEvent.updateDictionary({required Locale dictionary}) = _UpdateDictionarySettingsEvent; +} + +@immutable +final class _UpdateThemeSettingsEvent extends SettingsEvent { + const _UpdateThemeSettingsEvent({required this.appTheme}); + + /// The theme to update. + final AppTheme appTheme; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is _UpdateThemeSettingsEvent && other.appTheme == appTheme; + } + + @override + int get hashCode => appTheme.hashCode; + + @override + String toString() => 'SettingsEvent.updateTheme(appTheme: $appTheme)'; +} + +@immutable +final class _UpdateLocaleSettingsEvent extends SettingsEvent { + const _UpdateLocaleSettingsEvent({required this.locale}); + + /// The locale to update. + final Locale locale; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is _UpdateLocaleSettingsEvent && other.locale == locale; + } + + @override + int get hashCode => locale.hashCode; + + @override + String toString() => 'SettingsEvent.updateLocale(locale: $locale)'; +} + +@immutable +final class _UpdateDictionarySettingsEvent extends SettingsEvent { + const _UpdateDictionarySettingsEvent({required this.dictionary}); + + /// The dictionary to update. + final Locale dictionary; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is _UpdateDictionarySettingsEvent && other.dictionary == dictionary; + } + + @override + int get hashCode => dictionary.hashCode; + + @override + String toString() => 'SettingsEvent.updateDictionary(dictionary: $dictionary)'; +} diff --git a/pubspec.lock b/pubspec.lock index 0a9c27d..70deaed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: archive - sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.6.0" args: dependency: transitive description: @@ -265,28 +265,27 @@ packages: flutter_colorpicker: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "92bdb69a313a56c391ef148c12ef6539bd31253d" - url: "https://github.com/mchome/flutter_colorpicker" - source: git - version: "1.0.3" + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_gen_core: dependency: transitive description: name: flutter_gen_core - sha256: "3a6c3dbc1c0e260088e9c7ed1ba905436844e8c01a44799f6281edada9e45308" + sha256: b9894396b2a790cc2d6eb3ed86e5e113aaed993765b21d4b981c9da4476e0f52 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.5.0+1" flutter_gen_runner: dependency: "direct dev" description: name: flutter_gen_runner - sha256: "24889d5140b03997f7148066a9c5fab8b606dff36093434c782d7a7fb22c6fb6" + sha256: b4c4c54e4dd89022f5e405fe96f16781be2dfbeabe8a70ccdf73b7af1302c655 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.5.0+1" flutter_localizations: dependency: "direct main" description: flutter @@ -337,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hashcodes: + dependency: transitive + description: + name: hashcodes + sha256: "80f9410a5b3c8e110c4b7604546034749259f5d6dcca63e0d3c17c9258f1a651" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: transitive description: @@ -361,6 +368,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_size_getter: + dependency: transitive + description: + name: image_size_getter + sha256: f98c4246144e9b968899d2dfde69091e22a539bb64bc9b0bea51505fbb490e57 + url: "https://pub.dev" + source: hosted + version: "2.1.3" intl: dependency: "direct main" description: @@ -465,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider: dependency: transitive description: @@ -770,10 +793,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_ios: dependency: transitive description: @@ -830,6 +853,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.4.0" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -858,10 +897,10 @@ packages: dependency: transitive description: name: web_socket - sha256: bfe704c186c6e32a46f6607f94d079cd0b747b9a489fceeecc93cd3adb98edd5 + sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" web_socket_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e29fbdb..0cbcae9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: wordly description: Guess the WORD in six tries publish_to: none -version: 3.1.2+37 +version: 3.1.3+38 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' @@ -34,15 +34,14 @@ dependencies: logging: ^1.2.0 # Utils - flutter_colorpicker: - git: https://github.com/mchome/flutter_colorpicker + flutter_colorpicker: ^1.1.0 share_plus: ^9.0.0 url_launcher: ^6.2.6 dev_dependencies: build_runner: ^2.4.10 carapacik_lints: ^1.8.1 - flutter_gen_runner: ^5.4.0 + flutter_gen_runner: ^5.5.0+1 #flutter_launcher_icons: ^0.13.1 #flutter_native_splash: ^2.4.0 freezed: ^2.5.2